From 65897ce6c4d1e254c610c24d81d842e696041234 Mon Sep 17 00:00:00 2001 From: David Brazda Date: Wed, 22 Nov 2023 12:18:42 +0100 Subject: [PATCH] bugfix + optimizedcutoffs button on gui --- v2realbot/controller/services.py | 4 +- v2realbot/loader/trade_offline_streamer.py | 27 +- v2realbot/main.py | 17 +- v2realbot/reporting/archive/optimizecutoff.py | 166 ++++++++++++ .../archive/optimizecutoffprofloss.py | 167 ++++++++++++ .../reporting/archive/optimizecutoffv2.py | 169 ++++++++++++ v2realbot/reporting/optimizecutoffs.py | 241 ++++++++++++++++++ v2realbot/static/index.html | 1 + v2realbot/static/js/archivetables.js | 45 +++- 9 files changed, 830 insertions(+), 7 deletions(-) create mode 100644 v2realbot/reporting/archive/optimizecutoff.py create mode 100644 v2realbot/reporting/archive/optimizecutoffprofloss.py create mode 100644 v2realbot/reporting/archive/optimizecutoffv2.py create mode 100644 v2realbot/reporting/optimizecutoffs.py diff --git a/v2realbot/controller/services.py b/v2realbot/controller/services.py index fac1ec8..f6de492 100644 --- a/v2realbot/controller/services.py +++ b/v2realbot/controller/services.py @@ -37,7 +37,7 @@ from v2realbot.strategyblocks.indicators.indicators_hub import populate_dynamic_ from v2realbot.interfaces.backtest_interface import BacktestInterface import os from v2realbot.reporting.metricstoolsimage import generate_trading_report_image -import gc +#import gc #from pyinstrument import Profiler #adding lock to ensure thread safety of TinyDB (in future will be migrated to proper db) lock = Lock() @@ -524,7 +524,7 @@ def batch_run_manager(id: UUID, runReq: RunRequest, rundays: list[RunDay]): except Exception as e: print("Nepodarilo se vytvorit report image", str(e)+format_exc()) - gc.collect() + #gc.collect() #stratin run def run_stratin(id: UUID, runReq: RunRequest, synchronous: bool = False, inter_batch_params: dict = None): diff --git a/v2realbot/loader/trade_offline_streamer.py b/v2realbot/loader/trade_offline_streamer.py index ad68051..d57fd5f 100644 --- a/v2realbot/loader/trade_offline_streamer.py +++ b/v2realbot/loader/trade_offline_streamer.py @@ -22,7 +22,8 @@ from rich import print import queue from alpaca.trading.models import Calendar from tqdm import tqdm - +import time +from traceback import format_exc """ Trade offline data streamer, based on Alpaca historical data. """ @@ -101,6 +102,19 @@ class Trade_Offline_Streamer(Thread): #REFACTOR STARTS HERE #print(f"{self.time_from=} {self.time_to=}") + def get_calendar_with_retry(self, calendar_request, max_retries=3, delay=4): + attempts = 0 + while attempts < max_retries: + try: + cal_dates = self.clientTrading.get_calendar(calendar_request) + return cal_dates + except Exception as e: + attempts += 1 + if attempts >= max_retries: + raise + print(f"Attempt {attempts}: Error occurred - {e}. Retrying in {delay} seconds...") + time.sleep(delay) + if OFFLINE_MODE: #just one day - same like time_from den = str(self.time_to.date()) @@ -108,8 +122,15 @@ class Trade_Offline_Streamer(Thread): cal_dates = [bt_day] else: calendar_request = GetCalendarRequest(start=self.time_from,end=self.time_to) - cal_dates = self.clientTrading.get_calendar(calendar_request) - #ic(cal_dates) + + #toto zatim workaround - dat do retry funkce a obecne vymyslet exception handling, abych byl notifikovan a bylo videt okamzite v logu a na frontendu + try: + cal_dates = self.clientTrading.get_calendar(calendar_request) + except Exception as e: + print("CHYBA - retrying in 4s: " + str(e) + format_exc()) + time.sleep(5) + cal_dates = self.clientTrading.get_calendar(calendar_request) + #zatim podpora pouze main session #zatim podpora pouze 1 symbolu, predelat na froloop vsech symbolu ze symbpole diff --git a/v2realbot/main.py b/v2realbot/main.py index 0e673a5..1faca47 100644 --- a/v2realbot/main.py +++ b/v2realbot/main.py @@ -33,8 +33,9 @@ from time import sleep import v2realbot.reporting.metricstools as mt from v2realbot.reporting.metricstoolsimage import generate_trading_report_image from traceback import format_exc +from v2realbot.reporting.optimizecutoffs import find_optimal_cutoff #from async io import Queue, QueueEmpty - +# # install() # ic.configureOutput(includeContext=True) # def threadName(): @@ -561,6 +562,20 @@ def _generate_report_image(runner_ids: list[UUID]): except Exception as e: raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {str(e)}" + format_exc()) + +#TODO toto bude zaklad pro obecnou funkci, ktera bude volat ruzne analyzy +#vstupem bude obecny objekt, ktery ponese nazev analyzy + atributy +@app.post("/batches/optimizecutoff/{batch_id}", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"image/png": {}}}}) +def _generate_analysis(batch_id: str): + try: + res, stream = find_optimal_cutoff(batch_id=batch_id, steps=50, stream=True) + if res == 0: return StreamingResponse(stream, media_type="image/png",headers={"Content-Disposition": "attachment; filename=optimizedcutoff.png"}) + elif res < 0: + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {res}:{id}") + except Exception as e: + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {str(e)}" + format_exc()) + + #TestList APIS - do budoucna predelat SQL do separatnich funkci @app.post('/testlists/', dependencies=[Depends(api_key_auth)]) def create_record(testlist: TestList): diff --git a/v2realbot/reporting/archive/optimizecutoff.py b/v2realbot/reporting/archive/optimizecutoff.py new file mode 100644 index 0000000..2b36e60 --- /dev/null +++ b/v2realbot/reporting/archive/optimizecutoff.py @@ -0,0 +1,166 @@ +import matplotlib +import matplotlib.dates as mdates +#matplotlib.use('Agg') # Set the Matplotlib backend to 'Agg' +import matplotlib.pyplot as plt +import seaborn as sns +import pandas as pd +from datetime import datetime +from typing import List +from enum import Enum +import numpy as np +import v2realbot.controller.services as cs +from rich import print +from v2realbot.common.PrescribedTradeModel import TradeDirection, TradeStatus, Trade, TradeStoplossType +from v2realbot.utils.utils import isrising, isfalling,zoneNY, price2dec, safe_get#, print +from pathlib import Path +from v2realbot.config import WEB_API_KEY, DATA_DIR, MEDIA_DIRECTORY +from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account, OrderSide +from io import BytesIO +from v2realbot.utils.historicals import get_historical_bars +from alpaca.data.timeframe import TimeFrame, TimeFrameUnit +from collections import defaultdict +# Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere + + +#TODO: +#OPTIMALIZOVAT TENTO nebo DRUHY soubor +#Vyzkouset v realu +#ZKUSIT NA RELATIVNI PROFIT TAKE + +#ZAPRACOVAT DO GRAFU nebo do nejakeho toolu + + + +def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: bool = False): + + #TODO dopracovat drawdown a minimalni a maximalni profity nikoliv cumulovane, zamyslet se + #TODO list of runner_ids + #TODO pridelat na vytvoreni runnera a batche, samostatne REST API + na remove archrunnera + + if runner_ids is None and batch_id is None: + return -2, f"runner_id or batch_id must be present" + + if batch_id is not None: + res, runner_ids =cs.get_archived_runnerslist_byBatchID(batch_id) + + if res != 0: + print(f"no batch {batch_id} found") + return -1, f"no batch {batch_id} found" + + trades = [] + cnt_max = len(runner_ids) + cnt = 0 + #zatim zjistujeme start a end z min a max dni - jelikoz muze byt i seznam runner_ids a nejenom batch + end_date = None + start_date = None + for id in runner_ids: + cnt += 1 + #get runner + res, sada =cs.get_archived_runner_header_byID(id) + if res != 0: + print(f"no runner {id} found") + return -1, f"no runner {id} found" + + print("archrunner") + #print(sada) + + if cnt == 1: + start_date = sada.bt_from if sada.mode in [Mode.BT,Mode.PREP] else sada.started + if cnt == cnt_max: + end_date = sada.bt_to if sada.mode in [Mode.BT or Mode.PREP] else sada.stopped + # Parse trades + + trades_dicts = sada.metrics["prescr_trades"] + + for trade_dict in trades_dicts: + trade_dict['last_update'] = datetime.fromtimestamp(trade_dict.get('last_update')).astimezone(zoneNY) if trade_dict['last_update'] is not None else None + trade_dict['entry_time'] = datetime.fromtimestamp(trade_dict.get('entry_time')).astimezone(zoneNY) if trade_dict['entry_time'] is not None else None + trade_dict['exit_time'] = datetime.fromtimestamp(trade_dict.get('exit_time')).astimezone(zoneNY) if trade_dict['exit_time'] is not None else None + trades.append(Trade(**trade_dict)) + + #print(trades) + + # symbol = sada.symbol + # #hour bars for backtested period + # print(start_date,end_date) + # bars= get_historical_bars(symbol, start_date, end_date, TimeFrame.Hour) + # print("bars for given period",bars) + # """Bars a dictionary with the following keys: + # * high: A list of high prices + # * low: A list of low prices + # * volume: A list of volumes + # * close: A list of close prices + # * hlcc4: A list of HLCC4 indicators + # * open: A list of open prices + # * time: A list of times in UTC (ISO 8601 format) + # * trades: A list of number of trades + # * resolution: A list of resolutions (all set to 'D') + # * confirmed: A list of booleans (all set to True) + # * vwap: A list of VWAP indicator + # * updated: A list of booleans (all set to True) + # * index: A list of integers (from 0 to the length of the list of daily bars) + # """ + + # Filter to only use trades with status 'CLOSED' + closed_trades = [trade for trade in trades if trade.status == TradeStatus.CLOSED] + + print(closed_trades) + + if len(closed_trades) == 0: + return -1, "image generation no closed trades" + + # Group trades by date and calculate daily profits + trades_by_day = defaultdict(list) + for trade in trades: + if trade.status == TradeStatus.CLOSED and trade.exit_time: + trade_day = trade.exit_time.date() + trades_by_day[trade_day].append(trade) + + # Define a range of potential cutoff values + # min_profit = min(trade.profit for trade in trades if trade.profit is not None) + # max_profit = max(trade.profit for trade in trades if trade.profit is not None) + min_profit = 0 + max_profit = 1000 + potential_cutoffs = np.linspace(min_profit, max_profit, 100) + + # Function to calculate total profit for a given cutoff + def calculate_total_profit(cutoff): + total_profit = 0 + for day, day_trades in trades_by_day.items(): + daily_profit = 0 + for trade in day_trades: + daily_profit += trade.profit + if daily_profit >= cutoff: # Stop if daily profit reaches the cutoff + break + total_profit += min(daily_profit, cutoff) # Add the minimum of daily profit or cutoff + return total_profit + + # Evaluate each cutoff + cutoff_profits = {cutoff: calculate_total_profit(cutoff) for cutoff in potential_cutoffs} + + # Find the optimal cutoff + optimal_cutoff = max(cutoff_profits, key=cutoff_profits.get) + max_profit = cutoff_profits[optimal_cutoff] + + # Plotting + plt.figure(figsize=(10, 6)) + plt.plot(list(cutoff_profits.keys()), list(cutoff_profits.values()), label='Total Profit') + plt.scatter(optimal_cutoff, max_profit, color='red', label='Optimal Cutoff') + plt.title('Optimal Intra-Day Profit Cutoff Analysis') + plt.xlabel('Profit Cutoff') + plt.ylabel('Total Profit') + plt.legend() + plt.grid(True) + plt.savefig('optimal_cutoff.png') + + return optimal_cutoff, max_profit + + +# Example usage +# trades = [list of Trade objects] +if __name__ == '__main__': + # id_list = ["e8938b2e-8462-441a-8a82-d823c6a025cb"] + # generate_trading_report_image(runner_ids=id_list) + batch_id = "90973e57" + optimal_cutoff, max_profit = find_optimal_cutoff(batch_id=batch_id) + print(f"Optimal Cutoff: {optimal_cutoff}, Max Profit: {max_profit}") \ No newline at end of file diff --git a/v2realbot/reporting/archive/optimizecutoffprofloss.py b/v2realbot/reporting/archive/optimizecutoffprofloss.py new file mode 100644 index 0000000..f47d6b6 --- /dev/null +++ b/v2realbot/reporting/archive/optimizecutoffprofloss.py @@ -0,0 +1,167 @@ +import matplotlib +import matplotlib.dates as mdates +#matplotlib.use('Agg') # Set the Matplotlib backend to 'Agg' +import matplotlib.pyplot as plt +import seaborn as sns +import pandas as pd +from datetime import datetime +from typing import List +from enum import Enum +import numpy as np +import v2realbot.controller.services as cs +from rich import print +from v2realbot.common.PrescribedTradeModel import TradeDirection, TradeStatus, Trade, TradeStoplossType +from v2realbot.utils.utils import isrising, isfalling,zoneNY, price2dec, safe_get#, print +from pathlib import Path +from v2realbot.config import WEB_API_KEY, DATA_DIR, MEDIA_DIRECTORY +from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account, OrderSide +from io import BytesIO +from v2realbot.utils.historicals import get_historical_bars +from alpaca.data.timeframe import TimeFrame, TimeFrameUnit +from collections import defaultdict +# Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere + +#LOSS and PROFIT without GRAPH +def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: bool = False): + + #TODO dopracovat drawdown a minimalni a maximalni profity nikoliv cumulovane, zamyslet se + #TODO list of runner_ids + #TODO pridelat na vytvoreni runnera a batche, samostatne REST API + na remove archrunnera + + if runner_ids is None and batch_id is None: + return -2, f"runner_id or batch_id must be present" + + if batch_id is not None: + res, runner_ids =cs.get_archived_runnerslist_byBatchID(batch_id) + + if res != 0: + print(f"no batch {batch_id} found") + return -1, f"no batch {batch_id} found" + + trades = [] + cnt_max = len(runner_ids) + cnt = 0 + #zatim zjistujeme start a end z min a max dni - jelikoz muze byt i seznam runner_ids a nejenom batch + end_date = None + start_date = None + for id in runner_ids: + cnt += 1 + #get runner + res, sada =cs.get_archived_runner_header_byID(id) + if res != 0: + print(f"no runner {id} found") + return -1, f"no runner {id} found" + + print("archrunner") + #print(sada) + + if cnt == 1: + start_date = sada.bt_from if sada.mode in [Mode.BT,Mode.PREP] else sada.started + if cnt == cnt_max: + end_date = sada.bt_to if sada.mode in [Mode.BT or Mode.PREP] else sada.stopped + # Parse trades + + trades_dicts = sada.metrics["prescr_trades"] + + for trade_dict in trades_dicts: + trade_dict['last_update'] = datetime.fromtimestamp(trade_dict.get('last_update')).astimezone(zoneNY) if trade_dict['last_update'] is not None else None + trade_dict['entry_time'] = datetime.fromtimestamp(trade_dict.get('entry_time')).astimezone(zoneNY) if trade_dict['entry_time'] is not None else None + trade_dict['exit_time'] = datetime.fromtimestamp(trade_dict.get('exit_time')).astimezone(zoneNY) if trade_dict['exit_time'] is not None else None + trades.append(Trade(**trade_dict)) + + #print(trades) + + # symbol = sada.symbol + # #hour bars for backtested period + # print(start_date,end_date) + # bars= get_historical_bars(symbol, start_date, end_date, TimeFrame.Hour) + # print("bars for given period",bars) + # """Bars a dictionary with the following keys: + # * high: A list of high prices + # * low: A list of low prices + # * volume: A list of volumes + # * close: A list of close prices + # * hlcc4: A list of HLCC4 indicators + # * open: A list of open prices + # * time: A list of times in UTC (ISO 8601 format) + # * trades: A list of number of trades + # * resolution: A list of resolutions (all set to 'D') + # * confirmed: A list of booleans (all set to True) + # * vwap: A list of VWAP indicator + # * updated: A list of booleans (all set to True) + # * index: A list of integers (from 0 to the length of the list of daily bars) + # """ + + # Filter to only use trades with status 'CLOSED' + closed_trades = [trade for trade in trades if trade.status == TradeStatus.CLOSED] + + #print(closed_trades) + + if len(closed_trades) == 0: + return -1, "image generation no closed trades" + + # Group trades by date and calculate daily profits + trades_by_day = defaultdict(list) + for trade in trades: + if trade.status == TradeStatus.CLOSED and trade.exit_time: + trade_day = trade.exit_time.date() + trades_by_day[trade_day].append(trade) + + # Define ranges for loss and profit cutoffs + min_profit = 0 + max_profit = 1000 + profit_cutoffs = np.linspace(min_profit, max_profit, 20) + + min_loss = 0 + max_loss = -1000 + loss_cutoffs = np.linspace(min_loss, max_loss, 20) + #print(profit_cutoffs, loss_cutoffs) + # Function to calculate total profit for a given cutoff + def calculate_total_profit(profit_cutoff, loss_cutoff): + total_profit = 0 + for day, day_trades in trades_by_day.items(): + daily_profit = 0 + for trade in day_trades: + daily_profit += trade.profit + if daily_profit >= profit_cutoff or daily_profit <= loss_cutoff: + break + total_profit += daily_profit + return total_profit + + # Initialize variables to store optimal cutoffs and max total profit + optimal_profit_cutoff = max_profit + optimal_loss_cutoff = min_loss + max_total_profit = float('-inf') + + # Initialize a matrix to store total profits for each combination of cutoffs + total_profits_matrix = np.zeros((len(profit_cutoffs), len(loss_cutoffs))) + + for i, profit_cutoff in enumerate(profit_cutoffs): + for j, loss_cutoff in enumerate(loss_cutoffs): + total_profit = calculate_total_profit(profit_cutoff, loss_cutoff) + total_profits_matrix[i, j] = total_profit + if total_profit > max_total_profit: + max_total_profit = total_profit + optimal_profit_cutoff = profit_cutoff + optimal_loss_cutoff = loss_cutoff + + # Plotting the results as a heatmap + plt.figure(figsize=(20, 15)) + sns.heatmap(total_profits_matrix, xticklabels=np.round(loss_cutoffs, 2), yticklabels=np.round(profit_cutoffs, 2), + annot=True, fmt=".0f", cmap="viridis") #fmt=".0f", + #plasma, coolmap, inferno, viridis + plt.title("Total Profit for Combinations of Profit and Loss Cutoffs") + plt.xlabel("Loss Cutoff") + plt.ylabel("Profit Cutoff") + plt.savefig('optimal_cutoff.png') + + return optimal_profit_cutoff, optimal_loss_cutoff, max_total_profit + +# Example usage +# trades = [list of Trade objects] +if __name__ == '__main__': + # id_list = ["e8938b2e-8462-441a-8a82-d823c6a025cb"] + # generate_trading_report_image(runner_ids=id_list) + batch_id = "c76b4414" + optimal_profit_cutoff, optimal_loss_cutoff, max_profit = find_optimal_cutoff(batch_id=batch_id) + print(f"Optimal Profit Cutoff: {optimal_profit_cutoff}, Optimal Loss Cutoff: {optimal_loss_cutoff}, Max Profit: {max_profit}") \ No newline at end of file diff --git a/v2realbot/reporting/archive/optimizecutoffv2.py b/v2realbot/reporting/archive/optimizecutoffv2.py new file mode 100644 index 0000000..a92fe73 --- /dev/null +++ b/v2realbot/reporting/archive/optimizecutoffv2.py @@ -0,0 +1,169 @@ +import matplotlib +import matplotlib.dates as mdates +#matplotlib.use('Agg') # Set the Matplotlib backend to 'Agg' +import matplotlib.pyplot as plt +import seaborn as sns +import pandas as pd +from datetime import datetime +from typing import List +from enum import Enum +import numpy as np +import v2realbot.controller.services as cs +from rich import print +from v2realbot.common.PrescribedTradeModel import TradeDirection, TradeStatus, Trade, TradeStoplossType +from v2realbot.utils.utils import isrising, isfalling,zoneNY, price2dec, safe_get#, print +from pathlib import Path +from v2realbot.config import WEB_API_KEY, DATA_DIR, MEDIA_DIRECTORY +from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account, OrderSide +from io import BytesIO +from v2realbot.utils.historicals import get_historical_bars +from alpaca.data.timeframe import TimeFrame, TimeFrameUnit +from collections import defaultdict +# Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere + +#LOSS and PROFIT without GRAPH +def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: bool = False): + + #TODO dopracovat drawdown a minimalni a maximalni profity nikoliv cumulovane, zamyslet se + #TODO list of runner_ids + #TODO pridelat na vytvoreni runnera a batche, samostatne REST API + na remove archrunnera + + if runner_ids is None and batch_id is None: + return -2, f"runner_id or batch_id must be present" + + if batch_id is not None: + res, runner_ids =cs.get_archived_runnerslist_byBatchID(batch_id) + + if res != 0: + print(f"no batch {batch_id} found") + return -1, f"no batch {batch_id} found" + + trades = [] + cnt_max = len(runner_ids) + cnt = 0 + #zatim zjistujeme start a end z min a max dni - jelikoz muze byt i seznam runner_ids a nejenom batch + end_date = None + start_date = None + for id in runner_ids: + cnt += 1 + #get runner + res, sada =cs.get_archived_runner_header_byID(id) + if res != 0: + print(f"no runner {id} found") + return -1, f"no runner {id} found" + + print("archrunner") + #print(sada) + + if cnt == 1: + start_date = sada.bt_from if sada.mode in [Mode.BT,Mode.PREP] else sada.started + if cnt == cnt_max: + end_date = sada.bt_to if sada.mode in [Mode.BT or Mode.PREP] else sada.stopped + # Parse trades + + trades_dicts = sada.metrics["prescr_trades"] + + for trade_dict in trades_dicts: + trade_dict['last_update'] = datetime.fromtimestamp(trade_dict.get('last_update')).astimezone(zoneNY) if trade_dict['last_update'] is not None else None + trade_dict['entry_time'] = datetime.fromtimestamp(trade_dict.get('entry_time')).astimezone(zoneNY) if trade_dict['entry_time'] is not None else None + trade_dict['exit_time'] = datetime.fromtimestamp(trade_dict.get('exit_time')).astimezone(zoneNY) if trade_dict['exit_time'] is not None else None + trades.append(Trade(**trade_dict)) + + #print(trades) + + # symbol = sada.symbol + # #hour bars for backtested period + # print(start_date,end_date) + # bars= get_historical_bars(symbol, start_date, end_date, TimeFrame.Hour) + # print("bars for given period",bars) + # """Bars a dictionary with the following keys: + # * high: A list of high prices + # * low: A list of low prices + # * volume: A list of volumes + # * close: A list of close prices + # * hlcc4: A list of HLCC4 indicators + # * open: A list of open prices + # * time: A list of times in UTC (ISO 8601 format) + # * trades: A list of number of trades + # * resolution: A list of resolutions (all set to 'D') + # * confirmed: A list of booleans (all set to True) + # * vwap: A list of VWAP indicator + # * updated: A list of booleans (all set to True) + # * index: A list of integers (from 0 to the length of the list of daily bars) + # """ + + # Filter to only use trades with status 'CLOSED' + closed_trades = [trade for trade in trades if trade.status == TradeStatus.CLOSED] + + print(closed_trades) + + if len(closed_trades) == 0: + return -1, "image generation no closed trades" + + # Group trades by date and calculate daily profits + trades_by_day = defaultdict(list) + for trade in trades: + if trade.status == TradeStatus.CLOSED and trade.exit_time: + trade_day = trade.exit_time.date() + trades_by_day[trade_day].append(trade) + + # Define ranges for loss and profit cutoffs + min_profit = 50 + max_profit = 700 # Set an upper bound based on your data + profit_cutoffs = np.linspace(min_profit, max_profit, 50) # Adjust number of points as needed + + min_loss = -50 + max_loss = -700 # Assuming losses are negative values + loss_cutoffs = np.linspace(min_loss, max_loss, 50) + + def calculate_total_profit(profit_cutoff, loss_cutoff): + total_profit = 0 + for day, day_trades in trades_by_day.items(): + daily_profit = 0 + for trade in day_trades: + daily_profit += trade.profit + if daily_profit >= profit_cutoff or daily_profit <= loss_cutoff: + break + total_profit += daily_profit + return total_profit + + # Evaluate each combination of cutoffs + optimal_profit_cutoff = max_profit + optimal_loss_cutoff = min_loss + max_total_profit = float('-inf') + + for profit_cutoff in profit_cutoffs: + for loss_cutoff in loss_cutoffs: + total_profit = calculate_total_profit(profit_cutoff, loss_cutoff) + if total_profit > max_total_profit: + max_total_profit = total_profit + optimal_profit_cutoff = profit_cutoff + optimal_loss_cutoff = loss_cutoff + + print(f"Optimal Profit Cutoff: {optimal_profit_cutoff}, Optimal Loss Cutoff: {optimal_loss_cutoff}, Max Profit: {max_total_profit}") + + # Optional: Plot the results or return them for further analysis + + return optimal_profit_cutoff, optimal_loss_cutoff, max_total_profit + + # # Plotting + # plt.figure(figsize=(10, 6)) + # plt.plot(list(cutoff_profits.keys()), list(cutoff_profits.values()), label='Total Profit') + # plt.scatter(optimal_cutoff, max_profit, color='red', label='Optimal Cutoff') + # plt.title('Optimal Intra-Day Profit Cutoff Analysis') + # plt.xlabel('Profit Cutoff') + # plt.ylabel('Total Profit') + # plt.legend() + # plt.grid(True) + # plt.savefig('optimal_cutoff.png') + + # return optimal_cutoff, max_profit + +# Example usage +# trades = [list of Trade objects] +if __name__ == '__main__': + # id_list = ["e8938b2e-8462-441a-8a82-d823c6a025cb"] + # generate_trading_report_image(runner_ids=id_list) + batch_id = "c76b4414" + optimal_profit_cutoff, optimal_loss_cutoff, max_profit = find_optimal_cutoff(batch_id=batch_id) + print(f"Optimal Profit Cutoff: {optimal_profit_cutoff}, Optimal Loss Cutoff: {optimal_loss_cutoff}, Max Profit: {max_profit}") \ No newline at end of file diff --git a/v2realbot/reporting/optimizecutoffs.py b/v2realbot/reporting/optimizecutoffs.py new file mode 100644 index 0000000..42740bf --- /dev/null +++ b/v2realbot/reporting/optimizecutoffs.py @@ -0,0 +1,241 @@ +import matplotlib +import matplotlib.dates as mdates +#matplotlib.use('Agg') # Set the Matplotlib backend to 'Agg' +import matplotlib.pyplot as plt +import seaborn as sns +import pandas as pd +from datetime import datetime +from typing import List +from enum import Enum +import numpy as np +import v2realbot.controller.services as cs +from rich import print +from v2realbot.common.PrescribedTradeModel import TradeDirection, TradeStatus, Trade, TradeStoplossType +from v2realbot.utils.utils import isrising, isfalling,zoneNY, price2dec, safe_get#, print +from pathlib import Path +from v2realbot.config import WEB_API_KEY, DATA_DIR, MEDIA_DIRECTORY +from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account, OrderSide +from io import BytesIO +from v2realbot.utils.historicals import get_historical_bars +from alpaca.data.timeframe import TimeFrame, TimeFrameUnit +from collections import defaultdict +from scipy.stats import zscore +from io import BytesIO +# Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere + +#LOSS and PROFIT without GRAPH +def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: bool = False, rem_outliers:bool = False, file: str = "optimalcutoff.png",steps:int = 50): + + #TODO dopracovat drawdown a minimalni a maximalni profity nikoliv cumulovane, zamyslet se + #TODO list of runner_ids + #TODO pridelat na vytvoreni runnera a batche, samostatne REST API + na remove archrunnera + + if runner_ids is None and batch_id is None: + return -2, f"runner_id or batch_id must be present" + + if batch_id is not None: + res, runner_ids =cs.get_archived_runnerslist_byBatchID(batch_id) + + if res != 0: + print(f"no batch {batch_id} found") + return -1, f"no batch {batch_id} found" + + trades = [] + cnt_max = len(runner_ids) + cnt = 0 + #zatim zjistujeme start a end z min a max dni - jelikoz muze byt i seznam runner_ids a nejenom batch + end_date = None + start_date = None + for id in runner_ids: + cnt += 1 + #get runner + res, sada =cs.get_archived_runner_header_byID(id) + if res != 0: + print(f"no runner {id} found") + return -1, f"no runner {id} found" + + #print("archrunner") + #print(sada) + + if cnt == 1: + start_date = sada.bt_from if sada.mode in [Mode.BT,Mode.PREP] else sada.started + if cnt == cnt_max: + end_date = sada.bt_to if sada.mode in [Mode.BT or Mode.PREP] else sada.stopped + # Parse trades + + trades_dicts = sada.metrics["prescr_trades"] + + for trade_dict in trades_dicts: + trade_dict['last_update'] = datetime.fromtimestamp(trade_dict.get('last_update')).astimezone(zoneNY) if trade_dict['last_update'] is not None else None + trade_dict['entry_time'] = datetime.fromtimestamp(trade_dict.get('entry_time')).astimezone(zoneNY) if trade_dict['entry_time'] is not None else None + trade_dict['exit_time'] = datetime.fromtimestamp(trade_dict.get('exit_time')).astimezone(zoneNY) if trade_dict['exit_time'] is not None else None + trades.append(Trade(**trade_dict)) + + #print(trades) + + # symbol = sada.symbol + # #hour bars for backtested period + # print(start_date,end_date) + # bars= get_historical_bars(symbol, start_date, end_date, TimeFrame.Hour) + # print("bars for given period",bars) + # """Bars a dictionary with the following keys: + # * high: A list of high prices + # * low: A list of low prices + # * volume: A list of volumes + # * close: A list of close prices + # * hlcc4: A list of HLCC4 indicators + # * open: A list of open prices + # * time: A list of times in UTC (ISO 8601 format) + # * trades: A list of number of trades + # * resolution: A list of resolutions (all set to 'D') + # * confirmed: A list of booleans (all set to True) + # * vwap: A list of VWAP indicator + # * updated: A list of booleans (all set to True) + # * index: A list of integers (from 0 to the length of the list of daily bars) + # """ + + # Filter to only use trades with status 'CLOSED' + closed_trades = [trade for trade in trades if trade.status == TradeStatus.CLOSED] + + #print(closed_trades) + + if len(closed_trades) == 0: + return -1, "image generation no closed trades" + + # # Group trades by date and calculate daily profits + # trades_by_day = defaultdict(list) + # for trade in trades: + # if trade.status == TradeStatus.CLOSED and trade.exit_time: + # trade_day = trade.exit_time.date() + # trades_by_day[trade_day].append(trade) + + # Precompute daily cumulative profits + daily_cumulative_profits = defaultdict(list) + for trade in trades: + if trade.status == TradeStatus.CLOSED and trade.exit_time: + day = trade.exit_time.date() + daily_cumulative_profits[day].append(trade.profit) + + for day in daily_cumulative_profits: + daily_cumulative_profits[day] = np.cumsum(daily_cumulative_profits[day]) + + + if rem_outliers: + # Remove outliers based on z-scores + def remove_outliers(cumulative_profits): + all_profits = [profit[-1] for profit in cumulative_profits.values() if len(profit) > 0] + z_scores = zscore(all_profits) + print(z_scores) + filtered_profits = {} + for day, profits in cumulative_profits.items(): + if len(profits) > 0: + day_z_score = z_scores[list(cumulative_profits.keys()).index(day)] + if abs(day_z_score) < 3: # Adjust threshold as needed + filtered_profits[day] = profits + return filtered_profits + + daily_cumulative_profits = remove_outliers(daily_cumulative_profits) + + + # OPT1 Dynamically calculate profit_range and loss_range - based on eod daily profit + # all_final_profits = [profits[-1] for profits in daily_cumulative_profits.values() if len(profits) > 0] + # max_profit = max(all_final_profits) + # min_profit = min(all_final_profits) + # profit_range = (0, max_profit) if max_profit > 0 else (0, 0) + # loss_range = (min_profit, 0) if min_profit < 0 else (0, 0) + + # OPT2 Calculate profit_range and loss_range based on all cumulative profits + all_cumulative_profits = np.concatenate([profits for profits in daily_cumulative_profits.values()]) + max_cumulative_profit = np.max(all_cumulative_profits) + min_cumulative_profit = np.min(all_cumulative_profits) + profit_range = (0, max_cumulative_profit) if max_cumulative_profit > 0 else (0, 0) + loss_range = (min_cumulative_profit, 0) if min_cumulative_profit < 0 else (0, 0) + + print("Calculated ranges", profit_range, loss_range) + + num_points = steps # Adjust for speed vs accuracy + profit_cutoffs = np.linspace(*profit_range, num_points) + loss_cutoffs = np.linspace(*loss_range, num_points) + + # OPT 3Statically define ranges for loss and profit cutoffs + # profit_range = (0, 1000) # Adjust based on your data + # loss_range = (-1000, 0) + # num_points = 20 # Adjust for speed vs accuracy + + profit_cutoffs = np.linspace(*profit_range, num_points) + loss_cutoffs = np.linspace(*loss_range, num_points) + + total_profits_matrix = np.zeros((len(profit_cutoffs), len(loss_cutoffs))) + + for i, profit_cutoff in enumerate(profit_cutoffs): + for j, loss_cutoff in enumerate(loss_cutoffs): + total_profit = 0 + for daily_profit in daily_cumulative_profits.values(): + cutoff_index = np.where((daily_profit >= profit_cutoff) | (daily_profit <= loss_cutoff))[0] + if cutoff_index.size > 0: + total_profit += daily_profit[cutoff_index[0]] + else: + total_profit += daily_profit[-1] if daily_profit.size > 0 else 0 + total_profits_matrix[i, j] = total_profit + + # Find the optimal combination + optimal_idx = np.unravel_index(total_profits_matrix.argmax(), total_profits_matrix.shape) + optimal_profit_cutoff = profit_cutoffs[optimal_idx[0]] + optimal_loss_cutoff = loss_cutoffs[optimal_idx[1]] + max_profit = total_profits_matrix[optimal_idx] + + # Plotting + # Setting up dark mode for the plots + plt.style.use('dark_background') + + # Optionally, you can further customize colors, labels, and axes + params = { + 'axes.titlesize': 9, + 'axes.labelsize': 8, + 'xtick.labelsize': 9, + 'ytick.labelsize': 9, + 'axes.labelcolor': '#a9a9a9', #a1a3aa', + 'axes.facecolor': '#121722', #'#0e0e0e', #202020', # Dark background for plot area + 'axes.grid': False, # Turn off the grid globally + 'grid.color': 'gray', # If the grid is on, set grid line color + 'grid.linestyle': '--', # Grid line style + 'grid.linewidth': 1, + 'xtick.color': '#a9a9a9', + 'ytick.color': '#a9a9a9', + 'axes.edgecolor': '#a9a9a9' + } + plt.rcParams.update(params) + plt.figure(figsize=(10, 8)) + sns.heatmap(total_profits_matrix, xticklabels=np.rint(loss_cutoffs).astype(int), yticklabels=np.rint(profit_cutoffs).astype(int), cmap="viridis") + plt.xticks(rotation=90) # Rotate x-axis labels to be vertical + plt.yticks(rotation=0) # Keep y-axis labels horizontal + plt.gca().invert_yaxis() + plt.gca().invert_xaxis() + plt.suptitle("Total Profit for Combinations of Profit and Loss Cutoffs", fontsize=16) + plt.title(f"Optimal Profit Cutoff: {optimal_profit_cutoff:.2f}, Optimal Loss Cutoff: {optimal_loss_cutoff:.2f}, Max Profit: {max_profit:.2f}", fontsize=10) + plt.xlabel("Loss Cutoff") + plt.ylabel("Profit Cutoff") + + if stream is False: + plt.savefig(file) + plt.close() + print(f"Optimal Profit Cutoff(rem_outliers:{rem_outliers}): {optimal_profit_cutoff}, Optimal Loss Cutoff: {optimal_loss_cutoff}, Max Profit: {max_profit}") + return 0, None + else: + # Return the image as a BytesIO stream + img_stream = BytesIO() + plt.savefig(img_stream, format='png') + plt.close() + img_stream.seek(0) # Rewind the stream to the beginning + return 0, img_stream + +# Example usage +# trades = [list of Trade objects] +if __name__ == '__main__': + # id_list = ["e8938b2e-8462-441a-8a82-d823c6a025cb"] + # generate_trading_report_image(runner_ids=id_list) + batch_id = "c76b4414" + res, val = find_optimal_cutoff(batch_id=batch_id, file="optimal_cutoff_vectorized.png",steps=20) + #res, val = find_optimal_cutoff(batch_id=batch_id, rem_outliers=True, file="optimal_cutoff_vectorized_nooutliers.png") + + print(res,val) \ No newline at end of file diff --git a/v2realbot/static/index.html b/v2realbot/static/index.html index 6932148..83d1433 100644 --- a/v2realbot/static/index.html +++ b/v2realbot/static/index.html @@ -310,6 +310,7 @@ + diff --git a/v2realbot/static/js/archivetables.js b/v2realbot/static/js/archivetables.js index 7ad19bf..5a59e8b 100644 --- a/v2realbot/static/js/archivetables.js +++ b/v2realbot/static/js/archivetables.js @@ -210,6 +210,7 @@ $(document).ready(function () { $('#button_runagain_arch').attr('disabled','disabled'); $('#button_show_arch').attr('disabled','disabled'); $('#button_delete_arch').attr('disabled','disabled'); + $('#button_analyze').attr('disabled','disabled'); $('#button_edit_arch').attr('disabled','disabled'); $('#button_compare_arch').attr('disabled','disabled'); @@ -218,6 +219,7 @@ $(document).ready(function () { if ($(this).hasClass('selected')) { //$(this).removeClass('selected'); $('#button_show_arch').attr('disabled','disabled'); + $('#button_analyze').attr('disabled','disabled'); $('#button_runagain_arch').attr('disabled','disabled'); $('#button_delete_arch').attr('disabled','disabled'); $('#button_edit_arch').attr('disabled','disabled'); @@ -225,6 +227,7 @@ $(document).ready(function () { } else { //archiveRecords.$('tr.selected').removeClass('selected'); $(this).addClass('selected'); + $('#button_analyze').attr('disabled',false); $('#button_show_arch').attr('disabled',false); $('#button_runagain_arch').attr('disabled',false); $('#button_delete_arch').attr('disabled',false); @@ -413,12 +416,50 @@ $(document).ready(function () { } }); + //generate batch optimization cutoff (predelat na button pro obecne analyzy batche) + $('#button_analyze').click(function () { + row = archiveRecords.row('.selected').data(); + if (row == undefined || row.batch_id == undefined) { + return + } + console.log(row) + $('#button_analyze').attr('disabled','disabled'); + $.ajax({ + url:"/batches/optimizecutoff/"+row.batch_id, + beforeSend: function (xhr) { + xhr.setRequestHeader('X-API-Key', + API_KEY); }, + method:"POST", + xhrFields: { + responseType: 'blob' + }, + contentType: "application/json", + processData: false, + data: JSON.stringify(row.batch_id), + success:function(blob){ + var url = window.URL || window.webkitURL; + console.log("vraceny obraz", blob) + console.log("url",url.createObjectURL(blob)) + display_image(url.createObjectURL(blob)) + $('#button_analyze').attr('disabled',false); + }, + error: function(xhr, status, error) { + console.log("proc to skace do erroru?") + //window.alert(JSON.stringify(xhr)); + console.log(JSON.stringify(xhr)); + $('#button_analyze').attr('disabled',false); + } + }) + }); + + //generate report button $('#button_report').click(function () { rows = archiveRecords.rows('.selected'); if (rows == undefined) { return } + $('#button_report').attr('disabled','disabled'); runnerIds = [] if(rows.data().length > 0 ) { // Loop through the selected rows and display an alert with each row's ID @@ -444,11 +485,13 @@ $(document).ready(function () { console.log("vraceny obraz", blob) console.log("url",url.createObjectURL(blob)) display_image(url.createObjectURL(blob)) + $('#button_report').attr('disabled',false); }, error: function(xhr, status, error) { console.log("proc to skace do erroru?") //window.alert(JSON.stringify(xhr)); - console.log(JSON.stringify(xhr)); + console.log(JSON.stringify(xhr)); + $('#button_report').attr('disabled',false); } }) });