diff --git a/v2realbot/common/model.py b/v2realbot/common/model.py index 5cc289c..7dbc1b9 100644 --- a/v2realbot/common/model.py +++ b/v2realbot/common/model.py @@ -8,7 +8,6 @@ from pydantic import BaseModel from v2realbot.enums.enums import Mode, Account from alpaca.data.enums import Exchange - #models for server side datatables # Model for individual column data class ColumnData(BaseModel): @@ -55,6 +54,7 @@ class DataTablesRequest(BaseModel): #obecny vstup pro analyzera (vstupem muze byt bud batch_id nebo seznam runneru) class AnalyzerInputs(BaseModel): + function: str batch_id: Optional[str] = None runner_ids: Optional[List[UUID]] = None #additional parameter diff --git a/v2realbot/main.py b/v2realbot/main.py index 7c8035a..8f79e37 100644 --- a/v2realbot/main.py +++ b/v2realbot/main.py @@ -33,7 +33,8 @@ 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 v2realbot.reporting.optimizecutoffs import find_optimal_cutoff +import v2realbot.reporting.analyzer as ci #from async io import Queue, QueueEmpty # # install() @@ -591,19 +592,37 @@ def _generate_report_image(runner_ids: list[UUID]): #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", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"image/png": {}}}}) -def _generate_analysis(analyzerInputs: AnalyzerInputs): +def _optimize_cutoff(analyzerInputs: AnalyzerInputs): try: if len(analyzerInputs.runner_ids) == 0 and analyzerInputs.batch_id is None: raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: batch_id or runner_ids required") #bude predelano na obecny analyzator s obecnym rozhrannim - res, stream = find_optimal_cutoff(runner_ids=analyzerInputs.runner_ids, batch_id=analyzerInputs.batch_id, stream=True, **analyzerInputs.params) + res, stream = ci.find_optimal_cutoff.find_optimal_cutoff(runner_ids=analyzerInputs.runner_ids, batch_id=analyzerInputs.batch_id, stream=True, **analyzerInputs.params) 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()) +#obecna funkce pro analyzy +#vstupem bude obecny objekt, ktery ponese nazev analyzy + atributy +@app.post("/batches/analytics", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"image/png": {}}}}) +def _generate_analysis(analyzerInputs: AnalyzerInputs): + try: + if (analyzerInputs.runner_ids is None or len(analyzerInputs.runner_ids) == 0) and analyzerInputs.batch_id is None: + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: batch_id or runner_ids required") + + funct = "ci."+analyzerInputs.function+"."+analyzerInputs.function + custom_function = eval(funct) + stream = None + res, stream = custom_function(runner_ids=analyzerInputs.runner_ids, batch_id=analyzerInputs.batch_id, stream=True, **analyzerInputs.params) + + if res == 0: return StreamingResponse(stream, media_type="image/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)]) diff --git a/v2realbot/reporting/analyzer/__init__.py b/v2realbot/reporting/analyzer/__init__.py new file mode 100644 index 0000000..81e91da --- /dev/null +++ b/v2realbot/reporting/analyzer/__init__.py @@ -0,0 +1,8 @@ +import os + +for filename in os.listdir("v2realbot/reporting/analyzer"): + if filename.endswith(".py") and filename != "__init__.py": + # __import__(filename[:-3]) + __import__(f"v2realbot.reporting.analyzer.{filename[:-3]}") + #importlib.import_module() + diff --git a/v2realbot/reporting/analyzer/example_plugin.py b/v2realbot/reporting/analyzer/example_plugin.py new file mode 100644 index 0000000..c1d71c8 --- /dev/null +++ b/v2realbot/reporting/analyzer/example_plugin.py @@ -0,0 +1,203 @@ +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.model import AnalyzerInputs +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 +from v2realbot.reporting.load_trades import load_trades +from traceback import format_exc +# Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere + + +def example_plugin(runner_ids: list = None, batch_id: str = None, stream: bool = False, rem_outliers:bool = False, file: str = "optimalcutoff.png",steps:int = 50): + try: + res, trades, days = load_trades(runner_ids, batch_id) + if res < 0: + return (res, trades) + + cnt_max = days + #in trades is list of Trades + + #print(trades) + + ##THIS IS how you can fetch historical data for given period and for given TimeFrame (if needed in future) + # 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) + + # 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(f"Total Profit for Combinations of Profit/Loss Cutoffs ({cnt_max})", 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 + + except Exception as e: + # Detailed error reporting + return (-1, str(e) + format_exc()) + +# 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 = "73ad1866" + res, val = example_plugin(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/reporting/optimizecutoffs.py b/v2realbot/reporting/analyzer/find_optimal_cutoff.py similarity index 100% rename from v2realbot/reporting/optimizecutoffs.py rename to v2realbot/reporting/analyzer/find_optimal_cutoff.py diff --git a/v2realbot/reporting/analyzer/ls_profit_distribution.py b/v2realbot/reporting/analyzer/ls_profit_distribution.py new file mode 100644 index 0000000..b89402a --- /dev/null +++ b/v2realbot/reporting/analyzer/ls_profit_distribution.py @@ -0,0 +1,99 @@ +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.model import AnalyzerInputs +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 +from v2realbot.reporting.load_trades import load_trades +from typing import Tuple, Optional, List +from traceback import format_exc +# Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere + +def ls_profit_distribution(runner_ids: List = None, batch_id: str = None, stream: bool = False) -> Tuple[int, Optional[BytesIO]]: + try: + # Load trades + result, trades, days_cnt = load_trades(runner_ids, batch_id) + + # Proceed only if trades are successfully loaded + if result == 0: + # Filter trades based on direction and calculate profit + long_trades = [trade for trade in trades if trade.direction == TradeDirection.LONG] + short_trades = [trade for trade in trades if trade.direction == TradeDirection.SHORT] + + long_profits = [trade.profit for trade in long_trades] + short_profits = [trade.profit for trade in short_trades] + + # Setting up dark mode for visualization with custom parameters + plt.style.use('dark_background') + custom_params = { + 'axes.titlesize': 9, + 'axes.labelsize': 8, + 'xtick.labelsize': 9, + 'ytick.labelsize': 9, + 'axes.labelcolor': '#a9a9a9', + 'axes.facecolor': '#121722', + 'axes.grid': False, + 'grid.color': 'gray', + 'grid.linestyle': '--', + 'grid.linewidth': 1, + 'xtick.color': '#a9a9a9', + 'ytick.color': '#a9a9a9', + 'axes.edgecolor': '#a9a9a9' + } + plt.rcParams.update(custom_params) + + plt.figure(figsize=(10, 6)) + sns.histplot(long_profits, color='blue', label='Long Trades', kde=True) + sns.histplot(short_profits, color='red', label='Short Trades', kde=True) + plt.xlabel('Profit') + plt.ylabel('Number of Trades') + plt.title('Profit Distribution by Trade Direction') + plt.legend() + + # Handling the output + if stream: + img_stream = BytesIO() + plt.savefig(img_stream, format='png') + plt.close() + img_stream.seek(0) + return (0, img_stream) + else: + plt.savefig('profit_distribution.png') + plt.close() + return (0, None) + else: + return (-1, None) # Error handling in case of unsuccessful trade loading + + except Exception as e: + # Detailed error reporting + return (-1, str(e) + format_exc()) + + +# 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 = "73ad1866" + res, val = ls_profit_distribution(batch_id=batch_id) + #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/reporting/analyzer/profit_distribution_by_month.py b/v2realbot/reporting/analyzer/profit_distribution_by_month.py new file mode 100644 index 0000000..c0535f3 --- /dev/null +++ b/v2realbot/reporting/analyzer/profit_distribution_by_month.py @@ -0,0 +1,82 @@ +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.model import AnalyzerInputs +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 +from v2realbot.reporting.load_trades import load_trades +from typing import Tuple, Optional, List +from traceback import format_exc +# Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere + +def profit_distribution_by_month(runner_ids: List = None, batch_id: str = None, stream: bool = False) -> Tuple[int, BytesIO or None]: + try: + # Load trades + res, trades, days_cnt = load_trades(runner_ids, batch_id) + if res != 0: + raise Exception("Error in loading trades") + + # Filter trades by status and create DataFrame + df_trades = pd.DataFrame([t.dict() for t in trades if t.status == 'closed']) + + # Extract month and year from trade exit time + df_trades['month'] = df_trades['exit_time'].apply(lambda x: x.strftime('%Y-%m') if x is not None else None) + + # Group by direction and month, and sum the profits + grouped = df_trades.groupby(['direction', 'month']).profit.sum().unstack(fill_value=0) + + # Visualization + plt.style.use('dark_background') + fig, ax = plt.subplots(figsize=(10, 6)) + + # Plotting + grouped.T.plot(kind='bar', ax=ax) + + # Styling + ax.set_title('Profit Distribution by Month: Long vs Short') + ax.set_xlabel('Month') + ax.set_ylabel('Total Profit') + ax.legend(title='Trade Direction') + + # Adding footer + plt.figtext(0.99, 0.01, f'Days Count: {days_cnt}', horizontalalignment='right') + + # Save or stream + if stream: + img = BytesIO() + plt.savefig(img, format='png') + plt.close() + img.seek(0) + return (0, img) + else: + plt.savefig('profit_distribution_by_month.png') + plt.close() + return (0, None) + + except Exception as e: + # Detailed error reporting + return (-1, str(e) + format_exc()) + +# Local debugging +if __name__ == '__main__': + batch_id = "73ad1866" + res, val = profit_distribution_by_month(batch_id=batch_id) + print(res, val) \ No newline at end of file diff --git a/v2realbot/reporting/analyzer/profit_sum_by_hour.py b/v2realbot/reporting/analyzer/profit_sum_by_hour.py new file mode 100644 index 0000000..8665c75 --- /dev/null +++ b/v2realbot/reporting/analyzer/profit_sum_by_hour.py @@ -0,0 +1,106 @@ +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.model import AnalyzerInputs +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 +from v2realbot.reporting.load_trades import load_trades +from typing import Tuple, Optional, List +from traceback import format_exc +# Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere +def profit_sum_by_hour(runner_ids: list = None, batch_id: str = None, stream: bool = False, group_by: str = 'entry_time'): + try: + # Load trades + res, trades, days_cnt = load_trades(runner_ids, batch_id) + if res != 0: + raise Exception("Error in loading trades") + + # Filter closed trades + closed_trades = [trade for trade in trades if trade.status == 'closed'] + total_closed_trades = len(closed_trades) + + # Extract hour and profit/loss based on group_by parameter + hourly_profit_loss = {} + hourly_trade_count = {} + for trade in closed_trades: + # Determine the time attribute to group by + time_attribute = getattr(trade, group_by) if group_by in ['entry_time', 'exit_time'] else trade.entry_time + if time_attribute: + hour = time_attribute.hour + hourly_profit_loss.setdefault(hour, []).append(trade.profit) + hourly_trade_count[hour] = hourly_trade_count.get(hour, 0) + 1 + + # Aggregate profits and losses by hour + hourly_aggregated = {hour: sum(profits) for hour, profits in hourly_profit_loss.items()} + + # Visualization + hours = list(hourly_aggregated.keys()) + profits = list(hourly_aggregated.values()) + trade_counts = [hourly_trade_count.get(hour, 0) for hour in hours] + + plt.style.use('dark_background') + colors = ['blue' if profit >= 0 else 'orange' for profit in profits] + bars = plt.bar(hours, profits, color=colors) + + # Make the grid subtler + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.5) + + plt.xlabel('Hour of Day') + plt.ylabel('Profit/Loss') + plt.title(f'Distribution of Profit/Loss Sum by Hour ({group_by.replace("_", " ").title()})') + + # Add trade count and percentage inside the bars + for bar, count in zip(bars, trade_counts): + height = bar.get_height() + percent = (count / total_closed_trades) * 100 + # Position the text inside the bars + position = height - 20 if height > 0 else height + 20 + plt.text(bar.get_x() + bar.get_width() / 2., position, + f'{count} Trades\n({percent:.1f}%)', ha='center', va='center', color='white', fontsize=9) + + # Adjust footer position and remove large gap + footer_text = f'Days Count: {days_cnt} | Parameters: {{"runner_ids": {len(runner_ids) if runner_ids is not None else None}, "batch_id": {batch_id}, "stream": {stream}, "group_by": "{group_by}"}}' + plt.gcf().subplots_adjust(bottom=0.2) + plt.figtext(0.5, 0.02, footer_text, ha="center", fontsize=8, color='gray', bbox=dict(facecolor='black', edgecolor='none', pad=3.0)) + + # Output + if stream: + img = BytesIO() + plt.savefig(img, format='png', bbox_inches='tight') + plt.close() + img.seek(0) + return (0, img) + else: + plt.savefig('profit_loss_by_hour.png', bbox_inches='tight') + plt.close() + return (0, None) + + except Exception as e: + # Detailed error reporting + plt.close() + return (-1, str(e)) + +# Local debugging +if __name__ == '__main__': + batch_id = "9e990e4b" + # Example usage with group_by parameter + res, val = profit_sum_by_hour(batch_id=batch_id, group_by='exit_time') + print(res, val) \ No newline at end of file diff --git a/v2realbot/reporting/load_trades.py b/v2realbot/reporting/load_trades.py new file mode 100644 index 0000000..3563923 --- /dev/null +++ b/v2realbot/reporting/load_trades.py @@ -0,0 +1,70 @@ +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.model import AnalyzerInputs +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 +from typing import Tuple, Optional, List +from v2realbot.common.PrescribedTradeModel import TradeDirection, TradeStatus, Trade, TradeStoplossType + +def load_trades(runner_ids: List = None, batch_id: str = None) -> Tuple[int, List[Trade], int]: + if runner_ids is None and batch_id is None: + return -2, f"runner_id or batch_id must be present", 0 + + 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", 0 + + #DATA PREPARATION + 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", 0 + + #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)) + return 0, trades, cnt_max \ No newline at end of file diff --git a/v2realbot/static/index.html b/v2realbot/static/index.html index e4b2365..802081f 100644 --- a/v2realbot/static/index.html +++ b/v2realbot/static/index.html @@ -320,10 +320,11 @@ - + -
+ + - + @@ -872,12 +873,11 @@ - - +