From 2ecb90d83f5cc3bf5d8e35613558a19cffed12c7 Mon Sep 17 00:00:00 2001 From: David Brazda Date: Thu, 30 Nov 2023 14:11:03 +0100 Subject: [PATCH] dynamic toolbutts on json and plugin report system --- v2realbot/common/model.py | 2 +- v2realbot/main.py | 25 +- v2realbot/reporting/analyzer/__init__.py | 8 + .../reporting/analyzer/example_plugin.py | 203 +++++ .../find_optimal_cutoff.py} | 0 .../analyzer/ls_profit_distribution.py | 99 +++ .../analyzer/profit_distribution_by_month.py | 82 ++ .../reporting/analyzer/profit_sum_by_hour.py | 106 +++ v2realbot/reporting/load_trades.py | 70 ++ v2realbot/static/index.html | 10 +- v2realbot/static/index2.html | 184 +++++ v2realbot/static/js/config.js | 68 +- v2realbot/static/js/dynamicbuttons.js | 319 ++++++-- .../static/js/dynamicbuttons_oldModal.js | 140 ++++ .../js/tables/archivetable/functions.js | 21 +- .../static/js/tables/archivetable/handlers.js | 1 + .../static/js/tables/archivetable/init.js | 717 +++++++++--------- v2realbot/static/main.css | 93 ++- 18 files changed, 1705 insertions(+), 443 deletions(-) create mode 100644 v2realbot/reporting/analyzer/__init__.py create mode 100644 v2realbot/reporting/analyzer/example_plugin.py rename v2realbot/reporting/{optimizecutoffs.py => analyzer/find_optimal_cutoff.py} (100%) create mode 100644 v2realbot/reporting/analyzer/ls_profit_distribution.py create mode 100644 v2realbot/reporting/analyzer/profit_distribution_by_month.py create mode 100644 v2realbot/reporting/analyzer/profit_sum_by_hour.py create mode 100644 v2realbot/reporting/load_trades.py create mode 100644 v2realbot/static/index2.html create mode 100644 v2realbot/static/js/dynamicbuttons_oldModal.js 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 @@ - - + \ No newline at end of file diff --git a/v2realbot/static/index2.html b/v2realbot/static/index2.html new file mode 100644 index 0000000..003fc3e --- /dev/null +++ b/v2realbot/static/index2.html @@ -0,0 +1,184 @@ + + + + + + + + + + +
+ + +
+ + + + + + diff --git a/v2realbot/static/js/config.js b/v2realbot/static/js/config.js index 0435c38..571597f 100644 --- a/v2realbot/static/js/config.js +++ b/v2realbot/static/js/config.js @@ -9,9 +9,9 @@ // PRIMARY KEY("id" AUTOINCREMENT) // ); //novy komentar -configData = {} +let configData = {} -//pridat sem i config area +//sluzba z globalni promenne s JS configuraci dotahne dana data function get_from_config(name, def_value) { def_value = def_value ? def_value : null console.log("required", name, configData) @@ -25,52 +25,58 @@ function get_from_config(name, def_value) { } } -$(document).ready(function () { - const apiBaseUrl = ''; - // Function to populate the config list and load JSON data initially - function loadConfig(configName) { - const rec = new Object() - rec.item_name = configName +function loadConfig(configName) { + return new Promise((resolve, reject) => { + const rec = new Object(); + rec.item_name = configName; $.ajax({ - url: `${apiBaseUrl}/config-items-by-name/`, + url: `/config-items-by-name/`, beforeSend: function (xhr) { - xhr.setRequestHeader('X-API-Key', - API_KEY); }, - METHOD: 'GET', + xhr.setRequestHeader('X-API-Key', API_KEY); + }, + method: 'GET', contentType: "application/json", dataType: "json", data: rec, success: function (data) { - console.log(data) try { - configData[configName] = JSON.parse(data.json_data) - console.log(configData) - console.log("jsme tu") - indConfig = configData["JS"].indConfig - console.log("after") - //console.log(JSON.stringify(indConfig, null,null, 2)) - - console.log("before CHART_SHOW_TEXT",CHART_SHOW_TEXT) - var CHART_SHOW_TEXT = configData["JS"].CHART_SHOW_TEXT - console.log("after CHART_SHOW_TEXT",CHART_SHOW_TEXT) + var configData = JSON.parse(data.json_data); + resolve(configData); // Resolve the promise with configData } catch (error) { - window.alert(`Nešlo rozparsovat JSON_data string ${configName}`, error.message) + reject(error); // Reject the promise if there's an error } - }, error: function(xhr, status, error) { - var err = eval("(" + xhr.responseText + ")"); - window.alert(`Nešlo dotáhnout config nastaveni z db ${configName}`, JSON.stringify(xhr)); - console.log(JSON.stringify(xhr)); + reject(new Error(xhr.responseText)); // Reject the promise on AJAX error } }); + }); +} +function getConfiguration(area) { + return loadConfig(area).then(configData => { + console.log("Config loaded for", area, configData); + return configData; + }).catch(error => { + console.error('Error loading config for', area, error); + throw error; // Re-throw to allow caller to handle + }); +} + +//asynchrone naplni promennou +async function loadConfigData(jsConfigName) { + try { + configData[jsConfigName] = await getConfiguration(jsConfigName); + console.log("jsConfigName", jsConfigName); + } catch (error) { + console.error('Failed to load button configuration:',jsConfigName, error); } +} - const jsConfigName = "JS" - //naloadovan config - loadConfig(jsConfigName) +$(document).ready(function () { + var jsConfigName = "JS" + loadConfigData(jsConfigName) }); diff --git a/v2realbot/static/js/dynamicbuttons.js b/v2realbot/static/js/dynamicbuttons.js index a76e628..f59643f 100644 --- a/v2realbot/static/js/dynamicbuttons.js +++ b/v2realbot/static/js/dynamicbuttons.js @@ -1,66 +1,285 @@ //ekvivalent to ready $(function(){ - //dynamicke buttony predelat na trdi se vstupem (nazev cfg klice, id conteineru) - var buttonConfig = get_from_config("analyze_buttons"); + //load configu buttons + loadConfig("dynamic_buttons").then(config => { + console.log("Config loaded for dynamic_buttons", config); - console.log("here") + // $(targetElement).append(dropdownHtml) + // // Find the ul element within the dropdown + // var dropdownMenu = $(targetElement).find('.dropdown-menu'); + configData["dynamic_buttons"] = config + //toto je obecné nad table buttony + console.log("conf data z buttonu po loadu", configData) + populate_dynamic_buttons($("#buttons-container"), config); + }).catch(error => { + console.error('Error loading config for', "dynamic_buttons", error); + }); - buttonConfig.forEach(function(button) { - var $btnGroup = $('
', {class: 'btn-group'}); - var $btn = $('
' + + } + targetElement.append(dropdownHtml) + //console.log("po pridani", targetElement) + // Find the ul element within the dropdown + var dropdownMenu = targetElement.find('.dropdown-menu'); + + // Dynamically create buttons and forms based on the configuration + $.each(config, function(index, buttonConfig) { + var formHtml = createFormInputs(buttonConfig.additionalParameters, batch_id); + var batchInputHtml = batch_id ? '': '' + var buttonHtml = '
  • ' + buttonConfig.label + + '
    Loading...
    ' + + batchInputHtml + formHtml + '
  • '; + dropdownMenu.append(buttonHtml); + //$(targetElement).append(buttonHtml); + //$('#actionDropdown').next('.dropdown-menu').append(buttonHtml); + }); + + // Submit form logic + targetElement.find('.dropdown-menu').on('submit', '.action-form', function(e) { + e.preventDefault(); + + var $form = $(this); + var $submitButton = $form.find('input[type="submit"], button[type="submit"]'); // Locate the submit button + var $spinner = $form.find('#formSpinner'); + + // Serialize the form data to a JSON object + var formData = $form.serializeArray().reduce(function(obj, item) { + // Handle checkbox, translating to boolean + if ($form.find(`[name="${item.name}"]`).attr('type') === 'checkbox') { + obj[item.name] = item.value === 'on' ? true : false; + } else { + obj[item.name] = item.value; + } + //Number should be numbers, not strings + if ($form.find(`[name="${item.name}"]`).attr('type') === 'number') { + obj[item.name] = Number(item.value) + } + return obj; + }, {}); + + // puvodni bez boolean translatu + //var formData = $(this).serializeJSON(); + + //pokud nemame batch_id - dotahujeme rows ze selected runnerů + console.log("toto jsou formdata pred submitem", formData) + if (formData.batch_id == undefined) { + console.log("batch undefined") + rows = archiveRecords.rows('.selected'); + console.log(rows) + if (rows == undefined || rows.data().length == 0) { + console.log("no selected rows") + alert("no selected rows or batch_id") + return + } + // Creating an array to store the IDs + formData.runner_ids = [] + + // Iterating over the selected rows to extract the IDs + rows.every(function (rowIdx, tableLoop, rowLoop ) { + var data = this.data() + formData.runner_ids.push(data.id); + }); - $form.append($input); } - $btnGroup.append($btn).append($form); - $('#buttons-container').append($btnGroup); + //population of object that is expected by the endpoint + obj = {} + if (formData.runner_ids) { + obj.runner_ids = formData.runner_ids + delete formData.runner_ids + } + if (formData.batch_id) { + obj.batch_id = formData.batch_id + delete formData.batch_id + } + obj.function = formData.function + delete formData.function + obj.params = {} + obj.params = formData + + $submitButton.attr('disabled', true); + $spinner.removeClass('d-none'); - // Event listener for button - $btn.on('click', function(event) { - event.preventDefault(); - - var formData = $form.serializeArray().reduce(function(obj, item) { - obj[item.name] = item.value; - return obj; - }, {}); - - $.ajax({ - url: button.apiEndpoint, - method: 'POST', - data: formData, - success: function(response) { - console.log('API Call Successful:', response); - }, - error: function(error) { - console.error('API Call Failed:', error); + console.log("toto jsou transformovana data", obj) + var apiEndpoint = $(this).data('endpoint'); + // console.log("formdata", formData) + // API call (adjust as needed for your backend) + $.ajax({ + url: apiEndpoint, + beforeSend: function (xhr) { + xhr.setRequestHeader('X-API-Key', + API_KEY); }, + method: 'POST', + //menime hlavicku podle toho jestli je uspesne nebo ne, abychom mohli precist chybovou hlasku + xhr: function() { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.readyState === 2) { // Headers have been received + if (xhr.status === 200) { + xhr.responseType = "blob"; // Set responseType to 'blob' for successful image responses + } else { + xhr.responseType = "text"; // Set responseType to 'text' for error messages + } + } + }; + return xhr; + }, + xhrFields: { + responseType: 'blob' + }, + contentType: "application/json", + processData: false, + data: JSON.stringify(obj), + success: function(data, textStatus, xhr) { + if (xhr.getResponseHeader("Content-Type") === "image/png") { + // Process as Blob + var blob = new Blob([data], { type: 'image/png' }); + var url = window.URL || window.webkitURL; + display_image(url.createObjectURL(blob)); + } else { + // Process as JSON + console.log('Received JSON', data); } - }); + $submitButton.attr('disabled', false); + $spinner.addClass('d-none'); + }, + error: function(xhr, status, error) { + $spinner.addClass('d-none'); + $submitButton.attr('disabled', false); + console.log(xhr, status, error) + console.log(xhr.responseJSON.message) + if (xhr.responseJSON && xhr.responseJSON.detail) { + console.log('Error:', xhr.responseJSON.detail); + window.alert(xhr.responseJSON.detail); + } else { + // Fallback error message + console.log('Error:', error); + window.alert('An unexpected error occurred'); + } + } }); + console.log('Form submitted for', $(this).closest('.dropdown-item').text().trim()); }); -}); + + + //HANDLERS + + //CLICKABLE VERSION (odstranit d-none z action-formu) + // Attach click event to each dropdown item + // $('.dropdown-menu').on('click', '.dropdown-item', function(event) { + // event.stopPropagation(); // Stop the event from bubbling up + + // var currentForm = $(this).find('.action-form'); + // // Hide all other forms + // $('.action-form').not(currentForm).hide(); + // // Toggle current form + // currentForm.toggle(); + // }); + + // // Hide form when clicking outside + // $(document).on('click', function(event) { + // if (!$(event.target).closest('.dropdown-item').length) { + // $('.action-form').hide(); + // } + // }); + + // // Prevent global click event from hiding form when clicking inside a form + // $('.dropdown-menu').on('click', '.action-form', function(event) { + // event.stopPropagation(); + // }); + + + //VERZE on HOVER (je treba pridat class d-none do action formu) + // Toggle visibility of form on hover + targetElement.find('.dropdown-menu').on('mouseenter', '.dropdown-item', function() { + $(this).find('.action-form').removeClass('d-none').show(); + }).on('mouseleave', '.dropdown-item', function() { + setTimeout(() => { + if (!$('.action-form:hover').length) { + $(this).find('.action-form').addClass('d-none').hide(); + } + }, 50); + }); + + // // Hide form when mouse leaves the form area + // targetElement.find('.dropdown-menu').on('mouseleave', '.action-form', function() { + // $(this).hide(); + // }); + + // stop propagating click up + targetElement.find('.dropdown').on('click', function(event) { + // Stop the event from propagating to parent elements + event.stopPropagation(); + }); + + // stop propagating click up + targetElement.find('.action-form').on('click', function(event) { + // Stop the event from propagating to parent elements + event.stopPropagation(); + // Check if the clicked element or any of its parents is a submit button + if (!$(event.target).closest('input[type="submit"], button[type="submit"]').length) { + // Stop the event from propagating to parent elements + event.preventDefault(); + } + }); + +} \ No newline at end of file diff --git a/v2realbot/static/js/dynamicbuttons_oldModal.js b/v2realbot/static/js/dynamicbuttons_oldModal.js new file mode 100644 index 0000000..0f2c073 --- /dev/null +++ b/v2realbot/static/js/dynamicbuttons_oldModal.js @@ -0,0 +1,140 @@ +//ekvivalent to ready +$(function(){ + + // Toggle input fields based on the selected button + $('.main-btn, .dropdown-item').on('click', function(e) { + e.preventDefault(); + var targetId = $(this).data('target'); + + // Hide all input groups + $('.input-group').hide(); + + // Show the corresponding input group + $(targetId).show(); + }); + + // //load configu buttons + // loadConfig("dynamic_buttons").then(configData => { + // console.log("Config loaded for dynamic_buttons", configData); + // populate_dynamic_buttons(configData); + // }).catch(error => { + // console.error('Error loading config for', area, error); + // }); + + function populate_dynamic_buttons(buttonConfig) { + console.log("buttonConfig",buttonConfig) + + + buttonConfig.forEach(function(button) { + var modalId = 'modal-' + button.id; + var $btn = $('' + tools += '
    ' - // if (data.note) { - // better_counter = extractNumbersFromString(data.note); - // } - // try { - // profit = data.metrics.profit.batch_sum_profit; - // } catch (e) { - // profit = 'N/A'; - // } - // } - // } - // }); - - - //pokud mame batch_id podivame se zda jeho nastaveni uz nema a pokud ano pouzijeme to - //pokud nemame tak si ho loadneme - if (group) { - const existingBatch = batchHeaders.find(batch => batch.batch_id == group); - //jeste neni v poli batchu - udelame hlavicku - if (!existingBatch) { - itemCount = extractNumbersFromString(firstRowData.note); - profit = firstRowData.metrics.profit.batch_sum_profit; - period = firstRowData.note ? firstRowData.note.substring(0, 14) : ''; - started = firstRowData.started - stratinId = firstRowData.strat_id - var newBatchHeader = {batch_id:group, profit:profit, itemCount:itemCount, period:period, started:started, stratinId:stratinId} - batchHeaders.push(newBatchHeader) + //final closure + tools += '' + icon_color = getColorForId(stratinId) + profit_icon_color = (profit>0) ? "#4f8966" : "#bb2f5e" //"#d42962" } - //uz je v poli, ale mame novejsi (pribyl v ramci backtestu napr.) - updatujeme - else if (new Date(existingBatch.started) < new Date(firstRowData.started)) { - itemCount = extractNumbersFromString(firstRowData.note); - profit = firstRowData.metrics.profit.batch_sum_profit; - period = firstRowData.note ? firstRowData.note.substring(0, 14) : ''; - started = firstRowData.started - stratinId = firstRowData.id - existingBatch.itemCount = itemCount; - existingBatch.profit = profit; - existingBatch.period = period; - existingBatch.started = started; - } - //uz je v poli batchu vytahneme else { - profit = existingBatch.profit - itemCount = existingBatch.itemCount - period = existingBatch.period - started = existingBatch.started - stratinId = existingBatch.stratinId + //def color for no batch - semi transparent + icon_color = "#ced4da17" } - } + icon = ''+exp_coll_icon_name+'' - //zaroven nastavime u vsech childu - - // Construct the GROUP HEADER - sem pripadna tlačítka atp. - //var groupHeaderContent = '' + (group ? 'Batch ID: ' + group : 'No Batch') + ''; - var tools = '' - var icon = '' - icon_color = '' - profit_icon_color = '' - exp_coll_icon_name = '' - exp_coll_icon_name = (state == 'collapsed') ? 'expand_more' : 'expand_less' - if (group) { - tools = '' - tools += 'lab_profile' - tools += 'delete' - tools += 'csv' - tools += 'insert_drive_file' - tools += 'cut' - //final closure - tools += '' - icon_color = getColorForId(stratinId) - profit_icon_color = (profit>0) ? "#4f8966" : "#bb2f5e" //"#d42962" + //console.log(group, groupId, stratinId) + //var groupHeaderContent = ''+(group ? 'Batch ID: ' + group: 'No Batch')+''; + var groupHeaderContent = ''+ icon + (group ? 'Batch ID: ' + group: 'No Batch')+''; + groupHeaderContent += (group ? ' (' + itemCount + ')' + ' ' + period + ' Profit: ' + profit + '' : ''); + groupHeaderContent += group ? tools : "" + return $('') + .append('' + groupHeaderContent + '') + .attr('data-name', groupId) + .addClass('group-header') + .addClass(state); } - else { - //def color for no batch - semi transparent - icon_color = "#ced4da17" - } - icon = ''+exp_coll_icon_name+'' - - //console.log(group, groupId, stratinId) - //var groupHeaderContent = ''+(group ? 'Batch ID: ' + group: 'No Batch')+''; - var groupHeaderContent = ''+ icon + (group ? 'Batch ID: ' + group: 'No Batch')+''; - groupHeaderContent += (group ? ' (' + itemCount + ')' + ' ' + period + ' Profit: ' + profit + '' : ''); - groupHeaderContent += group ? tools : "" - return $('') - .append('' + groupHeaderContent + '') - .attr('data-name', groupId) - .addClass('group-header') - .addClass(state); + }, + drawCallback: function (settings) { + //console.log("drawcallback", configData) + setTimeout(function(){ + + //populate all tool buttons on batch header + // Loop over all divs with the class 'batch-buttons-container' + if (configData["dynamic_buttons"]) { + //console.log("jsme tu po cekani") + //console.log("pred loopem") + $('.batch_buttons_container').each((index, element) => { + //console.log("jsme uvnitr foreach"); + idecko = $(element).attr('id') + //console.log("idecko", idecko) + var batchId = $(element).data('batch-id'); // Get the data-batch-id attribute + //console.log("nalezeno pred", batchId, $(element)); + populate_dynamic_buttons($(element), configData["dynamic_buttons"], batchId); + //console.log("po", $(element)); + }); + }else { + console.log("no dynamic_buttons configuration loaded") + } + }, 1); + // var api = this.api(); + // var rows = api.rows({ page: 'current' }).nodes(); + + // api.column(17, { page: 'current' }).data().each(function (group, i) { + // console.log("drawCallabck i",i) + // console.log("rows", $(rows).eq(i)) + // var groupName = group ? group : $(rows).eq(i).attr('data-name'); + // console.log("groupName", groupName) + // var stateKey = 'dt-group-state-' + groupName; + // var state = localStorage.getItem(stateKey); + + // if (state === 'collapsed') { + // $(rows).eq(i).hide(); + // } else { + // $(rows).eq(i).show(); + // } + + // Set the unique identifier as a data attribute on each row + //$(rows).eq(i).attr('data-group-name', groupName); + + // // Add or remove the 'collapsed' class based on the state + // if (groupName.startsWith('no-batch-id-')) { + // $('tr[data-name="' + groupName + '"]').toggleClass('collapsed', state === 'collapsed'); + // } + // }); } - }, - // drawCallback: function (settings) { - // var api = this.api(); - // var rows = api.rows({ page: 'current' }).nodes(); - - // api.column(17, { page: 'current' }).data().each(function (group, i) { - // console.log("drawCallabck i",i) - // console.log("rows", $(rows).eq(i)) - // var groupName = group ? group : $(rows).eq(i).attr('data-name'); - // console.log("groupName", groupName) - // var stateKey = 'dt-group-state-' + groupName; - // var state = localStorage.getItem(stateKey); - - // if (state === 'collapsed') { - // $(rows).eq(i).hide(); - // } else { - // $(rows).eq(i).show(); - // } - - // // Set the unique identifier as a data attribute on each row - // //$(rows).eq(i).attr('data-group-name', groupName); - - // // // Add or remove the 'collapsed' class based on the state - // // if (groupName.startsWith('no-batch-id-')) { - // // $('tr[data-name="' + groupName + '"]').toggleClass('collapsed', state === 'collapsed'); - // // } - // }); - // } -}); + }); +} \ No newline at end of file diff --git a/v2realbot/static/main.css b/v2realbot/static/main.css index dd4a242..7ebc088 100644 --- a/v2realbot/static/main.css +++ b/v2realbot/static/main.css @@ -35,7 +35,9 @@ font-size: 19px; color: var(--bs-secondary); border-radius: 4px; - padding: 2px; + /* padding: 2px; */ + padding-left: 2px; + padding-right: 2px; } .tool-icon:hover { @@ -43,9 +45,72 @@ color: var(--bs-dark-bg-subtle); cursor: pointer; border-radius: 4px; - padding: 2px; + /* padding: 2px; */ } +.stat_div { + display: contents; +} + + /* Custom styles for dark mode and form offset */ + .dropdown-menu-dark .form-control, .dropdown-menu-dark .btn { + background-color: #343a40; + border-color: #6c757d; + color: white; + } + .dropdown-menu-dark .form-control:focus { + box-shadow: none; + border-color: #5cb85c; + } + .dropdown-item { + position: relative; + display: flex; + align-items: center; /* Align play icon vertically */ + } + .hover-icon { + margin-left: auto; /* Push play icon to the right */ + cursor: pointer; /* Change cursor on hover */ + } + .action-form { + z-index: 500000; + display: none; /* Hide form by default */ + position: absolute; + left: 100%; /* Position form to the right of the dropdown item */ + top: 0; + white-space: nowrap; /* Prevent wrapping on small screens */ + width: max-content; +/* Add some space between the dropdown item and the form */ + background: #343a40; /* Match the dropdown background color */ + border-radius: 0.25rem; /* Match Bootstrap's border radius */ + border: 1px solid #6c757d; /* Slight border for the form */ + } + /* .form-group { + display: flex; + gap: 0.5rem; + align-items: center; + margin-bottom: 0.5rem; + } */ + /* Floating label styles */ + .form-label-group { + position: relative; + /* padding-top: 15px; */ + } + .form-label-group label { + position: absolute; + top: 0; + left: 12px; + font-size: 75%; + /* transform: translateY(-50%); */ + margin-top: 0; /* Adjusted for font size */ + color: #6c757d; + pointer-events: none; + } + .form-label-group input, + .form-label-group select { + padding-top: 18px; + /* padding-bottom: 2px; */ + } + .pagination { --bs-pagination-padding-x: 0.45rem; --bs-pagination-padding-y: 0.15rem; @@ -271,7 +336,7 @@ table.dataTable thead>tr>th.sorting_asc:before, table.dataTable thead>tr>th.sort border-bottom: 10px solid #838E65; /* Triangle pointing down when expanded */ } - +.input-group { margin-top: 10px; display: none; } .group-header { cursor: pointer; @@ -280,33 +345,47 @@ table.dataTable thead>tr>th.sorting_asc:before, table.dataTable thead>tr>th.sort /* font-weight: bold; */ } -/* Hide all .batchtool_ elements by default */ +/* Hide all .batchtool elements by default */ .batchtool { - display: none; + display: inline-flex; /* Maintain the desired display type */ + opacity: 0; /* Initially fully transparent */ + visibility: hidden; /* Initially hidden */ + transition: opacity 0.5s, visibility 0s 0.5s; /* Transition for opacity and delay for visibility */ } -/* Show .batchtool elements when hovering over a .group-header row */ +/* Show .batchtool elements immediately when hovering over a .group-header row */ .group-header:hover .batchtool { - display: inline-block; /* or whatever display type suits your layout */ + opacity: 1; /* Fully opaque when hovered */ + visibility: visible; /* Visible when hovered */ + transition: opacity 0.5s, visibility 0s; /* Immediate transition for visibility */ +} + +/* Delay the transition when mouse leaves */ +.group-header .batchtool { + transition-delay: 0.5s; /* Delay the transition */ } .group-header .batchheader-profit-info { color: #3e999e; /* Highlight profit info */ + vertical-align: super; /* font-weight: bold; */ } .group-header .batchheader-count-info { color: #a1a1a1; /* Highlight count info */ + vertical-align: super; /* font-weight: bold; */ } .group-header .batchheader-batch-id { color: #a1a1a1; /* Highlight period info */ font-weight: 400; + vertical-align: super; } .group-header .batchheader-period-info { color: #a1a1a1; /* Highlight period info */ + vertical-align: super; /* font-weight: bold; */ }