Files
LSTM-PPO-Reinforcement-Lear…/bot.py
T

3076 lines
83 KiB
Python

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=3, 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, lookahead=36, 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):
end = min(i + lookahead + 1, n)
for j in range(i + min_distance, end):
# 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 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 &
(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 &
(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 BullishBB(df, lookahead=36):
bb = [0] * len(df)
high = df["High"].to_numpy()
low = df["Low"].to_numpy()
close = df["Close"].to_numpy()
bearish_ob = df["bearish_ob"].to_numpy()
n = len(df)
for i in range(n):
if bearish_ob[i] != 1:
continue
ob_high = high[i]
ob_low = low[i]
broken = False
for j in range(i + 1, min(i + lookahead + 1, n)):
# Break above the bearish OB
if not broken:
if close[j] > ob_high:
broken = True
else:
# Retest the breaker
if (
low[j] <= ob_high and
high[j] >= ob_low
):
bb[j] = 1
break
return bb
def BearishBB(df, lookahead=36):
bb = [0] * len(df)
high = df["High"].to_numpy()
low = df["Low"].to_numpy()
close = df["Close"].to_numpy()
bullish_ob = df["bullish_ob"].to_numpy()
n = len(df)
for i in range(n):
if bullish_ob[i] != 1:
continue
ob_high = high[i]
ob_low = low[i]
broken = False
for j in range(i + 1, min(i + lookahead + 1, n)):
if not broken:
if close[j] < ob_low:
broken = True
else:
if (
low[j] <= ob_high and
high[j] >= ob_low
):
bb[j] = 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, lookahead=36):
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):
if not np.isnan(bullish_ob[i]):
ob = bullish_ob[i]
for j in range(i + 1, min(i + lookahead + 1, n)):
if abs(low[j] - ob) <= threshold:
bull[j] = True
break
if not np.isnan(bearish_ob[i]):
ob = bearish_ob[i]
for j in range(i + 1, min(i + lookahead + 1, n)):
if abs(high[j] - ob) <= threshold:
bear[j] = True
break
return bull, bear
def FVGRetracement(df):
open_ = df["Open"].to_numpy()
close = df["Close"].to_numpy()
body_high = np.maximum(open_, close)
body_low = np.minimum(open_, close)
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 bull_fvg[i] == 0:
continue
lower = df["High"].iat[i - 1]
upper = df["Low"].iat[i + 1]
# Start from the upper FVG candle (i+1)
for j in range(i + 2, n):
# Stop when bullish candle appears
if close[j] >= open_[j]:
break
# Body overlaps FVG
if (
body_high[j] >= lower and
body_low[j] <= upper
):
bull_ret[j] = True
break
# Bearish FVG
for i in range(1, n - 1):
if bear_fvg[i] == 0:
continue
upper = df["Low"].iat[i - 1]
lower = df["High"].iat[i + 1]
for j in range(i + 2, n):
# Stop when bearish candle appears
if close[j] <= open_[j]:
break
if (
body_high[j] >= lower and
body_low[j] <= upper
):
bear_ret[j] = True
break
return bull_ret, bear_ret
def EQHEQLRetest(df, threshold=20, lookahead=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):
if eqh[i]:
level = max(high[i], body_high[i])
end = min(i + lookahead + 1, n)
for j in range(i + 1, end):
if (
abs(high[j] - level) <= threshold or
abs(body_high[j] - level) <= threshold and
df["bullish"].iloc[j]
):
eqh_retest[j] = True
break
if eql[i]:
level = min(low[i], body_low[i])
end = min(i + lookahead + 1, n)
for j in range(i + 1, end):
if (
abs(low[j] - level) <= threshold or
abs(body_low[j] - level) <= threshold and
df["bearish"].iloc[j]
):
eql_retest[j] = True
break
return eqh_retest, eql_retest
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, threshold=2):
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 * 0.1
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 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 VolumeMA(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.")
# 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 BuyScore(df):
return (
(df["EMA7"] > df["EMA21"]).astype(int) * 1 +
(df["EMA721_DIFF"] > 0).astype(int) * 1 +
(df["EMA7_Slope"] > 0).astype(int) * 1 +
(df["+di"] > df["-di"]).astype(int) * 1 +
(df["adx"] > 20).astype(int) * 1 +
(df["k"] > df["k_smooth"]).astype(int) * 1 +
(df["k"] < 20).astype(int) * 1 +
df["bullish_ob"] * 1 +
df["bullish_ob_mitigation"] * 1 +
df["bullish_mb"] * 1 +
df["bullish_fvg"] * 1 +
df["bullish_ifvg"] * 1 -
df["bearish_fvg"] * 1 -
df["bearish_ifvg"] * 1 -
df["eqh"] * 1 -
df["eql_retest"] * 1 +
df["bullish_rb"] * 1 +
df["bullish_mss"] * 1 +
df["bullish_bb"] * 1 +
# df["bullish_fib382"] * 1 +
# df["bullish_fib500"] * 1 +
# df["bullish_fib618"] * 1 +
df["bullish_fib786"] * 1 -
# df["bearish_fib382"] * 1 -
# df["bearish_fib500"] * 1 -
# df["bearish_fib618"] * 1 -
df["bearish_fib786"] * 1 -
(df["above_support"] == 0).astype(int) +
(df["below_resistance"] == 0).astype(int) * 1 +
df["above_vwap"] * 1 +
df["vwap_above_upper"] * 1 -
df["below_vwap"] * 1 -
df["vwap_below_lower"] * 1 -
df["bearish_rb"] * 1 -
df["bearish_ob"] * 1 -
df["bearish_ob_mitigation"] * 1 -
df["bearish_fvg"] * 1 +
df["eql"] * 1 +
df["eqh_retest"] * 1 -
df["bearish_mss"] * 1 -
df["bearish_bb"] * 1 -
(df["k"] >= 80) * 1
)
def SellScore(df):
return (
(df["EMA7"] < df["EMA21"]).astype(int) * 1 +
(df["EMA721_DIFF"] < 0).astype(int) * 1 +
(df["EMA7_Slope"] < 0).astype(int) * 1 +
(df["-di"] > df["+di"]).astype(int) * 1 +
(df["adx"] > 20).astype(int) * 1 +
(df["k"] < df["k_smooth"]).astype(int) * 1 +
(df["k"] > 80).astype(int) * 1 +
df["bearish_ob"] * 1 +
df["bearish_ob_mitigation"] * 1 +
df["bearish_mb"] * 1 +
df["bearish_fvg"] * 1 +
df["bearish_ifvg"] * 1 -
df["bullish_fvg"] * 1 -
df["bullish_ifvg"] * 1 -
df["eql"] * 1 -
df["eqh_retest"] * 1 +
df["bearish_mss"] * 1 +
df["bearish_bb"] * 1 +
# df["bearish_fib382"] * 1 +
# df["bearish_fib500"] * 1 +
# df["bearish_fib618"] * 1 +
df["bearish_fib786"] * 1 -
# df["bullish_fib382"] * 1 -
# df["bullish_fib500"] * 1 -
# df["bullish_fib618"] * 1 -
df["bullish_fib786"] * 1 +
(df["above_support"] == 0).astype(int) -
(df["below_resistance"] == 0).astype(int) * 1 +
df["below_vwap"] * 1 +
df["vwap_below_lower"] * 1 -
df["above_vwap"] * 1 -
df["vwap_above_upper"] * 1 -
df["bearish_rb"] * 1 -
df["bullish_rb"] * 1 -
df["bullish_ob"] * 1 -
df["bullish_ob_mitigation"] * 1 -
df["bullish_fvg"] * 1 +
df["eqh"] * 1 +
df["eql_retest"] * 1 -
df["bullish_mss"] * 1 -
df["bullish_bb"] * 1 -
(df["k"] <= 20) * 1
)
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['EMA1'] = EMA(df, 1)
# df['EMA71_DIFF'] = df['EMA1'] - df['EMA7']
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_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["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 = add_irl_erl(df)
df["sell_score"] = SellScore(df)
df["buy_score"] = BuyScore(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["volume_ma"] = VolumeMA(df)
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 = df[["Open", "High", "Low", "Close",
"k", "k_smooth", "adx", "+di", "-di", "EMA7", "EMA21", "EMA721_DIFF", "EMA7_Slope", "EMA21_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",
"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",
"volume_ma", "Volume",
"bullish", "bearish",
"asia_high_dist", "asia_low_dist",
"pdh_dist", "pdl_dist",
"pd_poc_dist",
"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 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 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 train_bot(symbol="XAUUSD"):
print("Training bot")
df = load_last_mb_xauusd()
print("Loading indicators...")
df = add_indicators(df)
print("Loaded indicators")
SEQ_LEN = 12 * 3
save_count = 1440
FEATURES = [
# "Open",
# "High",
# "Low",
# "Close",
"k",
"k_smooth",
"adx",
"+di",
"-di",
# "EMA7",
# "EMA21",
"EMA721_DIFF",
"EMA7_Slope",
"EMA21_Slope",
# "EMA71_DIFF",
"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_mss",
"bearish_mss",
"bullish_ob_mitigation",
"bearish_ob_mitigation",
"bullish_fvg_retracement",
"bearish_fvg_retracement",
"eqh_retest",
"eql_retest",
"exhaustion",
"above_support",
"below_resistance",
"bullish_fib382",
"bullish_fib500",
"bullish_fib618",
"bullish_fib786",
"bearish_fib382",
"bearish_fib500",
"bearish_fib618",
"bearish_fib786",
"range_high",
"range_low",
"erl_high",
"erl_low",
"irl",
# "vwap",
# "vwap_upper",
# "vwap_lower",
"above_vwap",
"below_vwap",
"vwap_above_upper",
"vwap_below_lower",
"vwap_slope",
"volume_ma",
"Volume",
"bullish",
"bearish",
"asia_high_dist",
"asia_low_dist",
"pdh_dist",
"pdl_dist",
"pd_poc_dist",
"sell_score",
"buy_score",
]
agent = xLSTMPPOAgent(
state_size=len(FEATURES),
hidden_size=64,
action_size=3
)
# knn = WinRateKNN(symbol)
"""
try:
agent.loadcheckpoint(symbol)
# knn.load()
print(f"[{symbol}] Loaded checkpoint")
except:
print(f"[{symbol}] Starting fresh")
"""
save_counter = 0
in_position = False
position_type = None
entry_price = 0
sl_price = 0
tp_price = 0
tp_hit = False
trade_returns = []
# STANDARD_SL_PIPS = 100
rr_list = []
SPREAD_AND_COMMISSION = 6
sl_pips = 0
sl_list = []
PIP_VALUE = 0.01
state_buffer = deque(maxlen=SEQ_LEN)
training_start_2 = time.time()
training_start_3 = time.time()
# preload sequence
for i in range(SEQ_LEN):
row = df.iloc[i][FEATURES].values.astype(np.float32)
state_buffer.append(row)
for i in range(SEQ_LEN, len(df)):
current = df.iloc[i]
base_sl = max(10, current["volume_ma"] / 100)
current_price = current["Close"]
high = current["High"]
low = current["Low"]
# TP1_PIPS = round(SL_PIPS * RR_RATIO, 0)
# relative_volume = (current["Volume"] / current["volume_ma"])
base_sl = max(10, current["volume_ma"] / 10)
state = current[FEATURES].values.astype(np.float32)
state_buffer.append(state)
if len(state_buffer) < SEQ_LEN:
continue
state_seq = np.array(state_buffer)
# === Select action ============================================
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)
# print(f"value: {value}")
# if df["adx"].iloc[i] < 20:
# action = 0
pnl = 0.0
done = False
# position_multiplier = 1
# ==============================================================
# OPEN LONG
# ==============================================================
# if action == 1 and not in_position and df["+di"].iloc[i] > df["-di"].iloc[i] and df["EMA_DIFF"].iloc[i] > 0 and df["k"].iloc[i] < 80:
# if action == 1 and not in_position and df["EMA7"].iloc[i] > df["EMA21"].iloc[i] and df["k"].iloc[i] < 80:
# if action == 1 and not in_position and df["EMA7_Slope"].iloc[i] > 0 and df["k"].iloc[i] < 80:
if action == 1 and not in_position:
"""
if df["above_vwap"].iloc[i] == 1:
position_multiplier = 2
if df["vwap_above_upper"].iloc[i] == 1:
position_multiplier = 3
"""
in_position = True
position_type = "long"
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)
# if value < 0:
# value = value * -1
tp_price = entry_price + (
base_sl * sl_mult * PIP_VALUE
# 2
)
pnl = 0.0
done = False
agent.store_transition(
state_seq,
action,
logprob,
value,
rr_mult,
sl_mult,
pnl,
done
)
tp_hit = False
# ==============================================================
# OPEN SHORT
# ==============================================================
# elif action == 2 and not in_position and df["-di"].iloc[i] > df["+di"].iloc[i] and df["EMA_DIFF"].iloc[i] < 0 and df["k"].iloc[i] > 20:
# elif action == 2 and not in_position and df["buy_score"].iloc[i] < df["sell_score"].iloc[i]:
# elif action == 2 and not in_position and df["EMA7"].iloc[i] < df["EMA21"].iloc[i] and df["k"].iloc[i] > 20:
# elif action == 2 and not in_position and df["EMA7_Slope"].iloc[i] < 0 and df["k"].iloc[i] > 20:
elif action == 2 and not in_position:
"""
if df["below_vwap"].iloc[i] == 1:
position_multiplier = 2
if df["vwap_below_lower"].iloc[i] == 1:
position_multiplier = 3
"""
in_position = True
position_type = "short"
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)
# if value < 0:
# value = value * -1
tp_price = entry_price - (
base_sl * rr_mult * PIP_VALUE
# 2
)
entry_price = current_price
reward = 0.0
pnl = 0.0
done = False
agent.store_transition(
state_seq,
action,
logprob,
value,
rr_mult,
sl_mult,
pnl,
done
)
tp_hit = False
# ==============================================================
# MANAGE POSITION
# ==============================================================
if not in_position:
done = False
pnl = 0
if in_position:
trade_closed = False
# ==================================================
# LONG
# ==================================================
if position_type == "long":
if not tp_hit and high >= tp_price:
# realized_reward += SL_PIPS - SPREAD_AND_COMMISSION
pnl = (sl_pips * rr_mult) - SPREAD_AND_COMMISSION
tp_hit = True
trade_closed = True
if not trade_closed and low <= sl_price:
pnl = ((sl_price - entry_price) / PIP_VALUE) - SPREAD_AND_COMMISSION
trade_closed = True
# ==================================================
# SHORT
# ==================================================
elif position_type == "short":
if not tp_hit and low <= tp_price:
pnl = (sl_pips * rr_mult) - SPREAD_AND_COMMISSION
tp_hit = True
trade_closed = True
if not trade_closed and high >= sl_price:
pnl = (entry_price - sl_price) / PIP_VALUE - SPREAD_AND_COMMISSION
trade_closed = True
if trade_closed:
in_position = False
done = True
trade_returns.append(pnl)
# ==============================================================
# STORE PPO TRANSITION
# ==============================================================
else:
action = 0
agent.store_transition(
state_seq,
action,
logprob,
value,
rr_mult,
sl_mult,
pnl,
done
)
save_counter += 1
# ==============================================================
# WEEKLY TRAINING
# ==============================================================
if save_counter % save_count == 0:
# ==========================================================
# WEEKLY STATS
# ==========================================================
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
rf = (
# (len(trade_returns) * winrate * RR_RATIO)
# - (len(trade_returns) * (1 - winrate))
# ) / (max_dd/SL_PIPS)
R_pnl/(max_dd/avg_sl)
)
print()
print("================================================")
print(f"[{symbol}] WEEKLY PPO TRAINING")
print("================================================")
print(f"Trades: {len(trade_returns)}")
print(f"Weekly PnL: {weekly_pnl/10:.0f} pips")
print(f"Winrate: {winrate*100:.2f}%")
print(f"Mean Win: {mean_win:.0f} pips")
print(f"Mean Loss: {mean_loss:.0f} pips")
print(f"Max DD: {max_dd*avg_rr/10:.2f}R")
print(f"PF: {profit_factor:.2f}")
print(f"RF: {round(R_pnl/(max_dd/(avg_sl/10)), 0):.0f}")
# print(f"RF: {round(rf, 0):.0f}")
print(f"Weekly R PnL: {R_pnl/10:.2f}R")
print(f"Sharpe: {sharpe:.2f}")
print(f"Sortino: {sortino:.2f}")
print("================================================")
print()
print(
f"[{symbol}] "
f"[INFO]"
f" Trained on data (Elapsed: {timedelta(seconds=int(time.time() - training_start_3))})"
)
trade_returns = []
# if len(agent.trajectory) >= 512:
training_start = time.time()
print(f"[{symbol}] [INFO] Training PPO...")
agent.train()
print(
f"[{symbol}] "
f"[INFO] Finished training PPO "
f"(Elapsed: {timedelta(seconds=int(time.time() - training_start))})"
)
# if save_counter % (1440 * 4) == 0:
agent.savecheckpoint(symbol)
completed = int((save_counter / save_count))
total = int(round(len(df) / 1440, 0))
elapsed = time.time() - training_start_2
avg_time = elapsed / max(completed, 1)
remaining = max(total - completed, 0)
eta = remaining * avg_time
print(
f"[{symbol}] [INFO] "
f"{completed}/{total} "
f"({completed/total*100:.1f}%) | "
f"Elapsed: {timedelta(seconds=int(elapsed))} | "
f"ETA: {timedelta(seconds=int(eta))}"
)
# training_start_2 = time.time()
training_start_3 = time.time()
sl_list = []
rr_list = []
# ==============================================================
# FINAL TRAINING
# ==============================================================
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)
# 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)
# 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 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",
# "EMA71_DIFF",
"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_mss",
"bearish_mss",
"bullish_ob_mitigation",
"bearish_ob_mitigation",
"bullish_fvg_retracement",
"bearish_fvg_retracement",
"eqh_retest",
"eql_retest",
"exhaustion",
"above_support",
"below_resistance",
"bullish_fib382",
"bullish_fib500",
"bullish_fib618",
"bullish_fib786",
"bearish_fib382",
"bearish_fib500",
"bearish_fib618",
"bearish_fib786",
"range_high",
"range_low",
"erl_high",
"erl_low",
"irl",
# "vwap",
# "vwap_upper",
# "vwap_lower",
"above_vwap",
"below_vwap",
"vwap_above_upper",
"vwap_below_lower",
"vwap_slope",
"volume_ma",
"Volume",
"bullish",
"bearish",
"asia_high_dist",
"asia_low_dist",
"pdh_dist",
"pdl_dist",
"pd_poc_dist",
"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]
# ==========================================================
# 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:
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
# )
open_long(
symbol,
risk_per_position,
sl_pips,
rr_mult
)
# 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
# )
open_short(
symbol,
risk_per_position,
sl_pips,
rr_mult
)
# else:
# print(
# f"[{symbol}] PPO HOLD"
# )
else:
print("new candle equal to old candle")
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()