3076 lines
83 KiB
Python
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()
|