added in MyLibs from private Mt5 repo

This commit is contained in:
Matt Corcoran
2025-07-12 15:53:31 +02:00
parent 136522ef1c
commit 1222236ded
14 changed files with 3357 additions and 0 deletions
@@ -0,0 +1 @@
.8448951187990853
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
import os
import logging
from datetime import datetime, timedelta
from time import sleep
import pandas as pd
from binance import Client
from forex_python.converter import CurrencyRates
import telegram
import schedule
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
)
# Environment variables for sensitive information
API_KEY = os.getenv("BINANCE_API_KEY")
API_SECRET = os.getenv("BINANCE_API_SECRET")
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
BOT_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
# Constants
MAX_TRADES = 5
RESET_STAKE_AMOUNT = 10 # minutes
DATA_DIR = "data"
# Ensure data directory exists
os.makedirs(DATA_DIR, exist_ok=True)
def telegram_send_message(bot_token, bot_chat_id, message):
"""
Send a message via Telegram bot.
"""
try:
bot = telegram.Bot(token=bot_token)
bot.send_message(chat_id=bot_chat_id, text=message)
except Exception as e:
logging.error(f"Failed to send Telegram message: {e}")
def update_df(gbp, usd, btc, exchange_rate):
"""
Update the HDF5 file with new balance data.
"""
try:
file_path = os.path.join(DATA_DIR, "balances.h5")
time_now = pd.to_datetime(datetime.now().replace(microsecond=0))
new_data = pd.DataFrame({
"date_time": [time_now],
"£": [gbp],
"$": [usd],
"BTC": [btc],
"Ex-rate": [exchange_rate],
})
with pd.HDFStore(file_path) as store:
if "df" in store:
df = store["df"]
df = pd.concat([df, new_data]).drop_duplicates(subset="date_time", keep="first")
else:
df = new_data.set_index("date_time")
store["df"] = df
logging.info("Updated DataFrame successfully.")
except Exception as e:
logging.error(f"Could not update DataFrame: {e}")
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, "Could not update DataFrame.")
def trim_df():
"""
Trim the DataFrame to the last 24 hours.
"""
try:
file_path = os.path.join(DATA_DIR, "balances.h5")
cut_before = datetime.now() - timedelta(hours=25)
with pd.HDFStore(file_path) as store:
if "df" in store:
df = store["df"]
df = df[df.index >= cut_before]
store["df"] = df
logging.info("Trimmed DataFrame to the last 24 hours.")
except Exception as e:
logging.error(f"Could not trim DataFrame: {e}")
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, "Could not trim DataFrame.")
def update_exchange_rate():
"""
Fetch the latest USD to GBP exchange rate and store it.
"""
cr = CurrencyRates()
file_path = os.path.join(DATA_DIR, "USDGBP_exchange_rate.txt")
try:
exchange_rate = cr.get_rate("USD", "GBP")
with open(file_path, "w") as f:
f.write(str(exchange_rate))
logging.info("Updated USD to GBP exchange rate.")
except Exception as e:
logging.error(f"Could not update exchange rate: {e}")
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, "Could not update exchange rate.")
def convert_usd_to_gbp(usd):
"""
Convert USD to GBP using the stored exchange rate.
"""
file_path = os.path.join(DATA_DIR, "USDGBP_exchange_rate.txt")
try:
with open(file_path, "r") as f:
exchange_rate = float(f.read())
gbp = usd * exchange_rate
return gbp, exchange_rate
except Exception as e:
logging.error(f"Could not convert USD to GBP: {e}")
return usd, 1.0 # Fallback to 1:1 conversion
def get_balance():
"""
Retrieve account balances from Binance and update the stake amount.
"""
try:
client = Client(API_KEY, API_SECRET)
account_info = client.get_account()
balances = account_info["balances"]
usdt = 0.0
for balance in balances:
asset = balance["asset"]
free = float(balance["free"])
locked = float(balance["locked"])
total = free + locked
if total > 0:
if asset == "USDT":
usdt += total
else:
try:
price = float(client.get_symbol_ticker(symbol=f"{asset}USDT")["price"])
usdt += total * price
except Exception:
pass
btc_price = float(client.get_symbol_ticker(symbol="BTCUSDT")["price"])
btc = usdt / btc_price
gbp, exchange_rate = convert_usd_to_gbp(usdt)
stake_amount = round(usdt / MAX_TRADES)
with open(os.path.join(DATA_DIR, "stake_amount.txt"), "w") as f:
f.write(str(stake_amount))
update_df(gbp, usdt, btc, exchange_rate)
except Exception as e:
logging.error(f"Could not fetch balances: {e}")
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, "Could not fetch Binance balances.")
def run_code():
"""
Main function to schedule tasks and run the bot.
"""
update_exchange_rate()
get_balance()
schedule.every(RESET_STAKE_AMOUNT).minutes.do(get_balance)
schedule.every().day.at("11:45").do(update_exchange_rate)
schedule.every(4).hours.do(trim_df)
while True:
schedule.run_pending()
sleep(1)
if __name__ == "__main__":
run_code()
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
import os
import time
import shutil
import warnings
import pandas as pd
import matplotlib.pyplot as plt # pip install matplotlib
import matplotlib.dates as mdates
from datetime import datetime, timedelta, date
import telegram
import schedule # pip install schedule
warnings.simplefilter(action='ignore', category=FutureWarning)
# Constants
LONG_PLOT_DAYS = -1 # 60 # -1 all days in df
LONG_PLOT_CURRENCY = '£'
PLOT_CURRENCY = '$'
# Environment variables for sensitive information
API_KEY = os.getenv("BINANCE_API_KEY")
API_SECRET = os.getenv("BINANCE_API_SECRET")
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
BOT_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
date_today = date.today()
TODAY = date_today.strftime("%Y_%m_%d")
def telegram_send_image(bot_token, bot_chat_id, image_path):
"""Send an image to the specified Telegram chat."""
bot = telegram.Bot(token=bot_token)
with open(image_path, 'rb') as photo:
bot.send_photo(chat_id=bot_chat_id, photo=photo)
return ()
def telegram_send_message(bot_token, bot_chat_id, message):
"""Send a message to the specified Telegram chat."""
bot = telegram.Bot(token=bot_token)
bot.send_message(chat_id=bot_chat_id, text=message)
return ()
def plot_and_send_image(df, currency, plot_title, file_name):
"""Generate plot and send it as an image to Telegram."""
plt.rcParams.update({'font.size': 14, 'font.family': 'STIXGeneral', 'mathtext.fontset': 'stix'})
fig, axs = plt.subplots(figsize=(7, 4))
axs.xaxis.set_major_formatter(mdates.DateFormatter("%d %b"))
df[currency].plot.line(ax=axs, color="darkgreen", linewidth=1.50)
delta_y = int(df[currency].max()) - int(df[currency].min())
y_min = int(df[currency].min()) - (delta_y * 0.05)
y_max = int(df[currency].max()) + (delta_y * 0.05)
x_max = datetime.now()
x_min = datetime.now() - timedelta(days=len(df))
axs.set_title(plot_title)
axs.set_ylim(y_min, y_max)
axs.set_xlim(x_min, x_max)
axs.set_ylabel("")
axs.set_xlabel("")
axs.grid(color='grey', alpha=0.5, linestyle='dashed', linewidth=0.5)
axs.yaxis.set_major_formatter(f"{currency} {{x:1.0f}}")
plt.savefig(file_name)
plt.cla()
plt.close(fig)
# Send image to Telegram
telegram_send_image(BOT_TOKEN, BOT_CHAT_ID, file_name)
return ()
def plot_long(period=LONG_PLOT_DAYS, currency=LONG_PLOT_CURRENCY):
"""Plot long-term data."""
try:
if os.path.exists('balances_24h.h5'):
balances_24h = pd.HDFStore('balances_24h.h5')
df = balances_24h['df_24h'].iloc[1:, :]
balances_24h.close()
if period == -1:
no_of_days = len(df)
else:
no_of_days = period
df = df.tail(no_of_days)
plot_and_send_image(df, currency, f"{no_of_days} days plot", "plot_long.png")
else:
message = "No balances_24h.h5 file"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
except Exception:
message = "Could not generate long plot"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
return ()
def plot_30days(currency=PLOT_CURRENCY):
"""Plot 30 days data."""
try:
if os.path.exists('balances_4h.h5'):
balances_4h = pd.HDFStore('balances_4h.h5')
df = balances_4h['df_4h'].iloc[1:, :]
balances_4h.close()
df = df.tail(180)
plot_and_send_image(df, currency, "30 Day Balances", "30_days.png")
else:
message = "No balances_4h.h5 file"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
except Exception:
message = "Could not generate 30 day plot"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
return ()
def plot_7days(currency=PLOT_CURRENCY):
"""Plot 7 days data."""
try:
if os.path.exists('balances_1h.h5'):
balances_1h = pd.HDFStore('balances_1h.h5')
df = balances_1h['df_1h']
end_date = datetime.now().replace(microsecond=0)
cut_before_date = end_date - timedelta(days=7)
df = df.loc[df.index >= cut_before_date]
plot_and_send_image(df, currency, "7 Day Balances", "7_days.png")
else:
message = "No balances_1h.h5 file"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
except Exception:
message = "Could not generate 7 day plot"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
return ()
def plot_24h(currency=PLOT_CURRENCY):
"""Plot 24 hours data."""
try:
if os.path.exists('balances.h5'):
balances = pd.HDFStore('balances.h5')
df = balances['df'].iloc[1:, :]
df['date_time'] = pd.to_datetime(df['date_time'])
balances.close()
df = df.set_index('date_time')
plot_and_send_image(df, currency, TODAY, "24_hour.png")
else:
message = "No balances.h5 file"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
except Exception:
message = "Could not generate 24h plot"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
return ()
def send_plots():
"""Send all generated plots to Telegram."""
try:
balances = pd.HDFStore('balances.h5')
df = balances['df']
balances.close()
GBP = df['£'].iloc[-1]
USDT = df['$'].iloc[-1]
BTC = df['BTC'].iloc[-1]
message = f"Balance:\n GBP £ {round(GBP, 2)}\n USD $ {round(USDT, 2)}\n BTC {round(BTC, 6)}"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
# Send all plots
for file_name in ["plot_long.png", "30_days.png", "7_days.png", "24_hour.png"]:
if os.path.exists(file_name):
telegram_send_image(BOT_TOKEN, BOT_CHAT_ID, file_name)
else:
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, f"Could not find {file_name}")
print(f"{datetime.now().replace(microsecond=0)} - Sent plots to Telegram.")
except Exception:
message = "Could not send plots."
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
return ()
def resample_data():
"""Resample data to 1h, 4h, and 24h."""
try:
if not os.path.exists("balances_1h.h5"):
balances = pd.HDFStore('balances.h5')
df = balances['df']
df['date_time'] = pd.to_datetime(df['date_time'])
df = df.set_index('date_time')
balances.close()
# Resample to 1h
data_1h = df.resample("H").mean()
balances_1h = pd.HDFStore('balances_1h.h5')
balances_1h['df_1h'] = data_1h
balances_1h.close()
if not os.path.exists("balances_4h.h5"):
balances_1h = pd.HDFStore('balances_1h.h5')
df_1h = balances_1h['df_1h']
balances_1h.close()
# Resample to 4h
data_4h = df_1h.resample("4H").mean()
balances_4h = pd.HDFStore('balances_4h.h5')
balances_4h['df_4h'] = data_4h
balances_4h.close()
if not os.path.exists("balances_24h.h5"):
balances_4h = pd.HDFStore('balances_4h.h5')
df_4h = balances_4h['df_4h']
balances_4h.close()
# Resample to 24h
data_24h = df_4h.resample("24H").mean()
balances_24h = pd.HDFStore('balances_24h.h5')
balances_24h['df_24h'] = data_24h
balances_24h.close()
except Exception:
message = "Error in resampling data"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
return ()
def delete_old_files():
"""Delete old files in the directory."""
try:
directory = "path_to_your_directory"
for file_name in os.listdir(directory):
file_path = os.path.join(directory, file_name)
if os.path.getmtime(file_path) < time.time() - 7 * 86400:
os.remove(file_path)
except Exception:
message = "Error in deleting old files"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
return ()
def archive_data():
"""Archive old data to a zip file."""
try:
archive_name = f"archive_data_{TODAY}.zip"
if not os.path.exists('archive_data'):
os.makedirs('archive_data')
shutil.make_archive(f'archive_data/{archive_name}', 'zip', 'path_to_your_directory')
except Exception:
message = "Error in archiving data"
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
return ()
# Scheduling tasks
schedule.every().day.at("00:00").do(archive_data)
schedule.every().day.at("01:00").do(delete_old_files)
schedule.every().day.at("02:00").do(resample_data)
schedule.every().day.at("02:30").do(plot_long)
schedule.every().day.at("03:00").do(plot_30days)
schedule.every().day.at("03:30").do(plot_7days)
schedule.every().day.at("04:00").do(plot_24h)
schedule.every().day.at("05:00").do(send_plots)
# Main loop
while True:
schedule.run_pending()
time.sleep(1)
@@ -0,0 +1 @@
4164
@@ -0,0 +1,699 @@
"""
MattC - 2025
This code is a working prototype and is intended for initial testing and development purposes. Some Python
standards, including but not limited to PEP 8 compliance, error handling, and code optimization, are yet to be fully
implemented. Further refactoring and enhancements are planned to improve readability, maintainability,
and efficiency.
"""
import os
from os import walk
import json
from datetime import datetime, timedelta
import pandas as pd
import shutil
from pathlib import Path
import logging
import matplotlib.pyplot as plt # pip install matplotlib
import matplotlib.dates as mdates
import warnings
import urllib
from urllib.request import urlopen
import time
warnings.simplefilter(action='ignore', category=FutureWarning)
logger = logging.getLogger(__name__)
# TODO sort out download data
class WalkForward(object):
def __init__(self, strategy_path, strategy, config, output_dir, wf_start, wf_finish, anchored_start, min_trades,
in_sample_days, out_sample_days, loss_function, cpu, epochs, wallet="2500", fee="0.002",
pre_live=False, re_opt=False, re_opt_t="5m 1h 1d"):
self.in_sample_days = in_sample_days
self.out_sample_days = out_sample_days
self.loss_function = loss_function
self.re_opt = re_opt
self.output_dir = self.create_run_dir(output_dir)
self.config = self.copy_input_config(config)
self.strategy_path = strategy_path
self.strategy_name = strategy
self.strategy = self.copy_input_strategy(strategy_path, self.strategy_name)
self.wf_start = wf_start
self.wf_finish = wf_finish
self.anchored_start = anchored_start
self.is_start = self.in_sample_start()
self.min_trades = min_trades
self.cpu = cpu
self.epochs = epochs
self.wallet = wallet
self.fee = fee
self.pre_live = pre_live
self.re_opt_t = re_opt_t
self.start_log()
def run_walk_forward(self):
stages = self.generate_wf_stages()
no_stages = len(stages)
start_wallet = self.wallet
start_time = datetime.now()
logger.info(f'Running Walk-forward for {no_stages} stages')
for count, value in enumerate(stages, 1):
stage_start_time = datetime.now()
logger.info(f'{"-" * 79}')
hyperopt_time = value[0]
backtest_time = value[1]
full_time_period = f"{hyperopt_time.split('-')[0]}-{backtest_time.split('-')[1]}"
logger.info(f'Walk-forward optimization for stage {count} of {no_stages}')
stage_dir = self.create_stage_dir(self.output_dir, count, full_time_period)
# Run Hyperopt and process required data
logger.info(f'Hyperopting for {hyperopt_time}')
cpu = self.set_cpu(hyperopt_time)
self.run_hyperopt(hyperopt_time, stage_dir, self.epochs, cpu)
hy_start = datetime.strptime(hyperopt_time.split('-')[0], "%Y%m%d")
hy_finish = datetime.strptime(hyperopt_time.split('-')[1], "%Y%m%d")
hy_delta = hy_start - hy_finish
hy_days = int(hy_delta.days)
logger.info(f'Running Hyperopt Backtest for period {hyperopt_time} ({hy_days} days)')
result_op, bt_file_op = self.run_backtest(hyperopt_time, "op_bt", start_wallet, stage_dir)
self.save_bt_file_data(stage_dir, bt_file_op, "op_bt")
df_op = self.update_df(result_op, "op_bt", count, hyperopt_time, start_wallet)
# Walk forward testing constant starting wallet:
logger.info(f'Running WF Backtest for period {backtest_time} ({self.out_sample_days} days)')
result_bt, bt_file_bt = self.run_backtest(backtest_time, "wf_bt", start_wallet, stage_dir)
self.save_bt_file_data(stage_dir, bt_file_bt, "wf_bt")
df_wf = self.update_df(result_bt, "wf_bt", count, backtest_time, start_wallet)
plot_equity_curve(df_wf, self.output_dir, save_fig=True)
self.combine_data(hyperopt_time, df_op, df_wf)
stage_run_time = str(datetime.now() - stage_start_time)
logger.info(f'Stage {count} walk-forward analysis Duration: {stage_run_time.split(".")[0]}')
if self.pre_live:
self.pre_live_optimise()
if self.re_opt:
self.re_optimise()
run_time = str(datetime.now() - start_time)
logger.info(f'Total walk-forward analysis Duration: {run_time.split(".")[0]}')
def re_optimise(self):
# Download data
# time_periods = "5m 15m 1h 4h 12h 1d"
time_periods = self.re_opt_t
self.download_data(time_periods)
end_date = datetime.strptime(self.wf_finish, "%Y%m%d")
start_date = end_date - timedelta(int(self.in_sample_days))
t1 = start_date.strftime("%Y%m%d")
t2 = end_date.strftime("%Y%m%d")
hyperopt_time = t1 + "-" + t2
full_time_period = f"{hyperopt_time}"
stage_dir = self.create_stage_dir(self.output_dir, "re_optimise", full_time_period)
epochs = f"{int(self.epochs)}"
self.run_hyperopt(hyperopt_time, stage_dir, epochs, self.cpu)
logger.info(f'Running Hyperopt Backtest for period {hyperopt_time} ({self.in_sample_days} days)')
_ = self.run_backtest(hyperopt_time, "re_optimise", self.wallet, stage_dir)
def save_bt_file_data(self, stage_dir, bt_file_op, file_id):
with open(f'{stage_dir}/{bt_file_op}') as f:
data1 = json.load(f)
df1 = pd.DataFrame.from_dict(data1["strategy"][self.strategy_name]["results_per_pair"])
csv_filepath = f"{stage_dir}/{file_id}_bt_results.csv"
df1.to_csv(csv_filepath)
def update_df(self, result, file_id, wf_stage, time_range, start_wallet):
h5_string = f"{self.output_dir}/{file_id}.h5"
if not os.path.exists(h5_string):
cols = ["profit_mean", "profit_mean_pct", "profit_sum", "profit_sum_pct", "profit_total_abs",
"profit_total_pct" "profit_total", "wins", "draws", "losses", "wf-stage", "bt_time_period",
"start-balance", "final-balance", "%_profit_pa", "acc-start", "acc-finish"]
df = pd.DataFrame(columns=cols)
df.to_hdf(h5_string, 'data')
df = pd.read_hdf(h5_string, 'data')
if file_id == "op_bt":
is_start = datetime.strptime(time_range.split('-')[0], "%Y%m%d")
is_finish = datetime.strptime(time_range.split('-')[1], "%Y%m%d")
delta = is_start - is_finish
sample_days = int(delta.days)
else:
sample_days = self.out_sample_days
data = result[0]
data.pop('key')
data["%_profit_pa"] = data["profit_total_pct"] / float(sample_days) * 365 # percent profit year
data["wf-stage"] = wf_stage
data["start-balance"] = start_wallet
data["final-balance"] = float(start_wallet) + float(data["profit_total_abs"])
data["bt_time_period"] = time_range
if len(df) == 0:
data["acc-start"] = float(self.wallet)
else:
data["acc-start"] = df["acc-finish"].iloc[-1]
data["acc-finish"] = (data["acc-start"] * data["profit_total_pct"] / 100) + data["acc-start"]
df2 = pd.DataFrame(data, index=[0])
df_new = df.copy()
df_new = df_new.append([df2], ignore_index=True)
csv_filepath = f"{self.output_dir}/{file_id}.csv"
df_new.to_csv(csv_filepath)
df_new.to_hdf(h5_string, 'data')
return df_new
def pre_live_optimise(self):
end_date = datetime.strptime(self.wf_finish, "%Y%m%d")
start_date = end_date - timedelta(int(self.in_sample_days))
t1 = start_date.strftime("%Y%m%d")
t2 = end_date.strftime("%Y%m%d")
hyperopt_time = t1 + "-" + t2
full_time_period = f"{hyperopt_time}"
stage_dir = self.create_stage_dir(self.output_dir, "pre_live", full_time_period)
epochs = f"{int(self.epochs) * 2}"
self.run_hyperopt(hyperopt_time, stage_dir, epochs, self.cpu)
logger.info(f'Running Hyperopt Backtest for period {hyperopt_time} ({self.in_sample_days} days)')
result_op = self.run_backtest(hyperopt_time, "pre_live", self.wallet, stage_dir)
def combine_data(self, hyp_time_range, df_op, df_wf):
is_start = datetime.strptime(hyp_time_range.split('-')[0], "%Y%m%d")
is_finish = datetime.strptime(hyp_time_range.split('-')[1], "%Y%m%d")
delta = is_start - is_finish
days_in = int(delta.days)
days_out = int(self.out_sample_days)
try:
a = pd.Series(df_op["profit_mean_pct"], name='op_profit_av')
b = pd.Series(df_wf["profit_mean_pct"], name='wf_profit_av')
c = pd.Series(df_op["wins"] / df_op["losses"], name='op_wl%')
d = pd.Series(df_wf["wins"] / df_wf["losses"], name='wf_wl%')
e = pd.Series(df_op['trades'].apply(lambda x: x / days_in), name='op_trades_per_day')
f = pd.Series(df_wf['trades'].apply(lambda x: x / days_out), name='wf_trades_per_day')
g = pd.Series(df_op["profit_total"].apply(lambda x: (x / days_in) * 365), name='op_ppa')
h = pd.Series(df_wf["profit_total"].apply(lambda x: (x / days_out) * 365), name='wf_ppa')
i = pd.Series(df_op["%_profit_pa"], name='op_%ppa')
j = pd.Series(df_wf["%_profit_pa"], name='wf_%ppa')
df_combined = pd.concat([a, b, c, d, e, f, g, h, i, j], axis=1)
filepath = f"{self.output_dir}/combined.csv"
df_combined.to_csv(filepath)
except:
pass
return
def generate_wf_stages(self):
is_start = datetime.strptime(self.is_start, "%Y%m%d")
oos_start = datetime.strptime(self.wf_start, "%Y%m%d")
end_date = datetime.strptime(self.wf_finish, "%Y%m%d")
oos_end = oos_start + timedelta(int(self.out_sample_days))
stages = []
while True:
t1 = is_start.strftime("%Y%m%d")
t2 = oos_start.strftime("%Y%m%d")
t3 = oos_end.strftime("%Y%m%d")
# define stage:
insample_timframe = t1 + "-" + t2
outsample_timframe = t2 + "-" + t3
stage = [insample_timframe, outsample_timframe]
stages.append(stage)
oos_start = oos_start + timedelta(int(self.out_sample_days))
oos_end = oos_start + timedelta(int(self.out_sample_days))
if not self.in_sample_days == "anchored":
is_start = is_start + timedelta(int(self.out_sample_days))
if oos_end > end_date:
break
return stages
def run_hyperopt(self, time_range, stage_dir, epochs, cpu):
"""
:param stage_number:
:param time_range:
:param epochs:
:param loss_function: SortinoHyperOptLoss,
:param fee:
:param cpu:
:return:
"""
self.wait_for_internet_connection()
start_time = datetime.now()
os.system(
"freqtrade hyperopt" +
" --min-trades " + self.min_trades +
" -j " + cpu +
" -e " + epochs +
" --spaces buy " +
" --fee " + self.fee +
" --logfile " + stage_dir + "/op_log" +
" --timerange " + time_range +
" --hyperopt-loss " + self.loss_function +
" --strategy " + self.strategy +
" --strategy-path " + self.output_dir +
" --config " + self.config +
" --dry-run-wallet " + self.wallet
)
self.wait_for_internet_connection()
os.system(
"freqtrade hyperopt-list" +
" --no-details " +
" --export-csv " + stage_dir + "/op.csv"
)
# # Copy the best optimisation results to "stage output directory":
src = Path(f"{self.output_dir}/{self.strategy}.json")
dst = f"{stage_dir}/op_result.json"
shutil.copyfile(str(src), dst)
run_time = str(datetime.now() - start_time)
logger.info(f'Hyperopt Duration: {run_time.split(".")[0]}')
with open(dst) as f:
data = json.load(f)
results = data["params"]["buy"]
for i in results:
logger.info(f'Hyperopt result: {i}: {results[i]}')
def run_backtest(self, time_range, file_id, wallet, stage_dir):
self.wait_for_internet_connection()
start_time = datetime.now()
os.system(
"freqtrade backtesting" +
" --export trades " +
" --fee " + self.fee +
f" --logfile {stage_dir}/{file_id}_log.txt"
" --timerange " + time_range +
" --strategy " + self.strategy +
" --strategy-path " + self.output_dir +
" --config " + self.config +
" --dry-run-wallet " + wallet +
f" --export-filename {stage_dir}/{file_id}_result.json"
)
result, bt_file = self.get_backtest_data(stage_dir, file_id)
run_time = str(datetime.now() - start_time)
if not file_id == "wf_acc_bt":
logger.info(f'Backtest Duration: {run_time.split(".")[0]}')
self.log_bt_results(time_range, result, wallet, file_id)
self.plot_bt_profit(time_range, stage_dir, bt_file, file_id)
return result, bt_file
def download_data(self, time_periods, days="4000"):
self.wait_for_internet_connection()
logger.info(f'downloading data')
os.system(
"freqtrade download-data" +
" -t " + time_periods +
" --exchange binance " +
" --pairs .*/USDT " +
" --new-pairs-days " + days +
" --include-inactive-pairs "
)
logger.info(f'finished downloading data')
return
@staticmethod
def wait_for_internet_connection():
start_time = datetime.now()
switch = True
while True:
try:
urlopen('https://www.google.com', timeout=1)
if not switch:
offline_time = str(datetime.now() - start_time)
logger.warning(f"Disconnected time: {offline_time.split('.')[0]}")
logger.warning("#############################")
return
except urllib.error.URLError:
if switch:
logger.warning("#############################")
logger.warning("NO INTERNET")
switch = False
time.sleep(2)
pass
def log_bt_results(self, time_range, result, wallet, file_id):
data = result[0].copy()
final_balance = float(wallet) + data["profit_total_abs"]
w_start = round(float(wallet), 2)
w_finish = round(final_balance, 2)
percent_prof = round(data["profit_total_pct"], 1)
if file_id == "op_bt":
is_start = datetime.strptime(time_range.split('-')[0], "%Y%m%d")
is_finish = datetime.strptime(time_range.split('-')[1], "%Y%m%d")
delta = is_start - is_finish
sample_days = int(delta.days)
else:
sample_days = self.out_sample_days
pppa = round((percent_prof / float(sample_days) * 365), 2) # percent profit per year
logger.info(f'Backtest result - Balance £{w_start} --> £{w_finish} ({percent_prof}%): {pppa} %profit pa')
# Trade stats
win = data['wins']
loss = data['losses']
draw = data['draws']
trades = data['trades']
logger.info(f"Backtest result - Wins: {win}, Draws: {draw}, Losses: {loss}, trades: {trades}")
# Average tade profits:
mean_p = round(float(data['profit_mean']), 2)
mp_percent = round(float(data['profit_mean_pct']), 2)
logger.info(f"Backtest result - mean trade profit £{mean_p}, {mp_percent}%")
# Account draw-down:
dd_percent = round((data['max_drawdown_account'] * 100), 2)
dd_abs = round(float(data['max_drawdown_abs']), 2)
logger.info(f"Backtest result - Max dd: {dd_percent}%, £{dd_abs}")
def plot_bt_profit(self, time_range, stage_dir, bt_file, file_id):
os.system(
"freqtrade plot-profit "
" --timeframe 1d "
" --timerange " + time_range +
" --strategy " + self.strategy +
" --strategy-path " + self.output_dir +
" --config " + self.config +
f" --export-filename {stage_dir}/{bt_file}"
)
# TODO relative path required:
src = Path(f"/home/matt/freqtrade/user_data/plot/freqtrade-profit-plot.html")
dst = f"{stage_dir}/{file_id}_profit-plot.html"
shutil.copyfile(str(src), dst)
@staticmethod
def get_backtest_data(stage_dir, file_id):
f = []
for (dirpath, dirnames, filenames) in walk(stage_dir):
f.extend(filenames)
break
# Find the backtest file for wf stage
for file in f:
# not meta.json:
if file[-9:-5] != "meta":
my_file = file_id + "_result"
if file.split("-")[0] == my_file:
# change to while open:
f = open(stage_dir + "/" + file)
data = json.load(f)
result = data["strategy_comparison"]
backtest_file = file
return result, backtest_file
def create_run_dir(self, path):
_time = datetime.now().strftime('%Y%m%d_%I:%M%p')
if self.re_opt:
directory = f"{path}/{_time}_{self.loss_function}_re-optimise_{self.in_sample_days}"
else:
directory = f"{path}/{_time}_{self.loss_function}_in_{self.in_sample_days}_out_{self.out_sample_days}"
if not os.path.exists(path):
os.makedirs(path)
try:
os.makedirs(directory)
except:
pass
return directory
@staticmethod
def create_stage_dir(path, stage, full_time_period):
dir_string = f"{path}/stage_{stage}_{full_time_period}"
if not os.path.exists(path):
os.makedirs(path)
try:
os.makedirs(dir_string)
except Exception as e:
pass
return dir_string
def copy_input_config(self, in_config):
# Copy config:
config = f"{in_config}"
dst_config = f"{self.output_dir}/{config.split('/')[-1]}"
shutil.copyfile(config, dst_config)
return dst_config
def copy_input_strategy(self, in_strategy_path, strategy):
# Copy Strategy
src = f"{in_strategy_path}/{strategy}.py"
dst_strategy = f"{self.output_dir}/{strategy}.py"
shutil.copyfile(src, dst_strategy)
return strategy
def in_sample_start(self):
oos_start = datetime.strptime(self.wf_start, "%Y%m%d")
if self.in_sample_days == "anchored":
is_start = datetime.strptime(self.anchored_start, "%Y%m%d") # + oos days
else:
is_start = oos_start - timedelta(int(self.in_sample_days))
is_start = is_start.strftime("%Y%m%d")
return is_start
def set_cpu(self, hyperopt_time):
is_start = datetime.strptime(hyperopt_time.split('-')[0], "%Y%m%d")
is_finish = datetime.strptime(hyperopt_time.split('-')[1], "%Y%m%d")
delta = is_finish - is_start
days = int(delta.days)
if self.in_sample_days == "anchored":
if days < 50:
cpu = "-1"
if days > 50:
cpu = "-2"
if days > 100:
cpu = "-4"
if days > 150:
cpu = "-6"
if days > 200:
cpu = "-8"
if days > 250:
cpu = "-10"
if days > 300:
cpu = "-11"
if days > 350:
cpu = "-12"
if days > 400:
cpu = "-13"
if days > 450:
cpu = "-14"
if days > 500:
cpu = "-14"
if days > 550:
cpu = "-16"
if days > 600:
cpu = "-16"
if days > 650:
cpu = "-17"
if days > 700:
cpu = "-17"
if days > 750:
cpu = "-17"
if days > 800:
cpu = "-18"
if days > 850:
cpu = "-18"
if days > 900:
cpu = "-18"
if days > 950:
cpu = "-19"
if days > 1000:
cpu = "-19"
if days > 1100:
cpu = "-20"
else:
d1 = datetime.strptime("20220601", "%Y%m%d")
d2 = datetime.strptime("20210101", "%Y%m%d")
d3 = datetime.strptime("20200101", "%Y%m%d")
d4 = datetime.strptime("20190101", "%Y%m%d")
d5 = datetime.strptime("20180101", "%Y%m%d")
if is_finish > d1:
cpu = self.cpu
if d2 < is_finish < d1:
cpu = int(self.cpu) + 1
if d3 < is_finish < d2:
cpu = int(self.cpu) + 2
if d4 < is_finish < d3:
cpu = int(self.cpu) + 3
if d5 < is_finish < d4:
cpu = int(self.cpu) + 4
if is_finish < d5:
cpu = int(self.cpu) + 5
return str(cpu)
def start_log(self):
# Set format and level:
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - WF - %(levelname)s - %(message)s')
# Remove old handlers:
while logger.handlers:
logger.handlers.pop()
# Define file handler:
file_handler = logging.FileHandler(f'{self.output_dir}/wf.log')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Define console handler:
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# Startup logs:
_cpu = 20 + int(self.cpu) + 1
start_date = datetime.strptime(self.is_start, '%Y%m%d').date()
end_date = datetime.strptime(self.wf_finish, '%Y%m%d').date()
run_time = str(end_date - start_date).split(",")[0]
logger.info('Code Initiated')
logger.info(f'Strategy:{self.strategy_path}/{self.strategy}')
logger.info(f'Config:{self.config}')
logger.info(f'Loss Function: {self.loss_function}')
logger.info(f'Time Frame:{self.is_start}-{self.wf_finish} ({run_time})')
logger.info(f'IS-days:{self.in_sample_days}, OOS-days:{self.out_sample_days}')
logger.info(f'CPUs:{_cpu}, Epochs:{self.epochs}, Wallet:{"2500"}, Fee:{"0.002"}, Min-trades:{self.min_trades}')
def plot_equity_curve(df, output_dir, save_fig=False):
df = df.copy()
pd.set_option('display.max_columns', None)
fig, axs = plt.subplots(figsize=(7, 4))
axs.xaxis.set_major_formatter(mdates.DateFormatter("%d %b"))
df['dates'] = df["bt_time_period"].apply(lambda i: i.split("-")[1])
df['dt'] = df['dates'].apply(lambda i: datetime.strptime(i, '%Y%m%d'))
df.set_index('dt')
df.plot(ax=axs, x="dt", y="acc-finish")
dela_y = int(df["acc-finish"].max()) - int(df["acc-finish"].min())
y_min = int(df["acc-finish"].min()) - (dela_y * 0.05)
y_max = int(df["acc-finish"].max()) + (dela_y * 0.05)
axs.set_title('Walk-Forward equity curve')
axs.set_ylim(y_min, y_max)
axs.set_ylabel("")
axs.set_xlabel("")
axs.grid(color='grey', alpha=0.5, linestyle='dashed', linewidth=0.5)
axs.yaxis.set_major_formatter("£" + '{x:1.0f}')
# plt.show()
if save_fig:
try:
plt.savefig(f"{output_dir}/wf_equity_curve.png")
except:
pass
def walk_forward(path, strategy, config, output_dir, wf_start, wf_finish, anchored_start, pre_live=False, re_opt=False):
is_list = ["730"]
oos_list = ["30"]
n_trades = ["100"]
cpu_list = ["-15"]
ep = "100"
loss_f = "SharpeHyperOptLoss"
for count, is_days in enumerate(is_list):
oos_days = oos_list[count]
nt = n_trades[count]
cores = cpu_list[count]
wf = WalkForward(strategy_path=path, strategy=strategy, config=config, output_dir=output_dir,
wf_start=wf_start, wf_finish=wf_finish, anchored_start=anchored_start, epochs=ep,
loss_function=loss_f, in_sample_days=is_days, out_sample_days=oos_days, min_trades=nt,
cpu=cores, pre_live=pre_live, re_opt=re_opt)
wf.run_walk_forward()
return
def re_optimise(path, strategy, config, output_dir, cpu="-19"):
today = datetime.now().strftime("%Y%m%d")
loss_f = "SortinoHyperOptLoss"
in_sample_days = "730"
n_trades = "100"
ep = "200"
download_data_t = "5m 1h 1d"
wf = WalkForward(strategy_path=path, strategy=strategy, config=config, output_dir=output_dir, wf_start=today,
wf_finish=today, anchored_start=today, epochs=ep, loss_function=loss_f,
in_sample_days=in_sample_days, out_sample_days="1", min_trades=n_trades, cpu=cpu,
pre_live=False, re_opt=True, re_opt_t=download_data_t)
wf.re_optimise()
if __name__ == "__main__":
WF_START = "20200101" # 2023-03-25
WF_END = "20230910"
ANCHORED_START = "20200101" # only used if in_sample_days == "anchored"
STRATEGY_PATH_THOR = "/home/matt/freqtrade/user_data/strategies/Thor"
STRATEGY_THOR = "Optimise_Thor_BuySig_RiskReward"
CONFIG_THOR = "/home/matt/freqtrade/user_data/strategies/Thor/config_Thor_WF.json"
OUTPUT_DIR_THOR = "/home/matt/freqtrade/user_data/strategies/Thor/walk_forward"
# -------------------------------------------------------------
walk_forward(STRATEGY_PATH_THOR, STRATEGY_THOR, CONFIG_THOR, OUTPUT_DIR_THOR, WF_START, WF_END, ANCHORED_START)
# -------------------------------------------------------------
re_optimise(STRATEGY_PATH_THOR, STRATEGY_THOR, CONFIG_THOR, OUTPUT_DIR_THOR)