dynamic toolbutts on json and plugin report system
This commit is contained in:
@ -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
|
||||
|
||||
@ -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)])
|
||||
|
||||
8
v2realbot/reporting/analyzer/__init__.py
Normal file
8
v2realbot/reporting/analyzer/__init__.py
Normal file
@ -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()
|
||||
|
||||
203
v2realbot/reporting/analyzer/example_plugin.py
Normal file
203
v2realbot/reporting/analyzer/example_plugin.py
Normal file
@ -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)
|
||||
99
v2realbot/reporting/analyzer/ls_profit_distribution.py
Normal file
99
v2realbot/reporting/analyzer/ls_profit_distribution.py
Normal file
@ -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)
|
||||
82
v2realbot/reporting/analyzer/profit_distribution_by_month.py
Normal file
82
v2realbot/reporting/analyzer/profit_distribution_by_month.py
Normal file
@ -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)
|
||||
106
v2realbot/reporting/analyzer/profit_sum_by_hour.py
Normal file
106
v2realbot/reporting/analyzer/profit_sum_by_hour.py
Normal file
@ -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)
|
||||
70
v2realbot/reporting/load_trades.py
Normal file
70
v2realbot/reporting/load_trades.py
Normal file
@ -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
|
||||
@ -320,10 +320,11 @@
|
||||
<button title="Export selected days to XML" id="button_export_xml" class="btn btn-outline-success btn-sm">Export xml</button>
|
||||
<button title="Export selected days to CSV" id="button_export_csv" class="btn btn-outline-success btn-sm">Export csv</button>
|
||||
<button title="For selected days generates basic report image." id="button_report" class="btn btn-outline-success btn-sm">Report(q)</button>
|
||||
<button title="For selected days creates heatmap for optimal profit/loss cutoffs" id="button_analyze" class="btn btn-outline-success btn-sm">Cutoffs Heatmap</button>
|
||||
<!-- <button title="For selected days creates heatmap for optimal profit/loss cutoffs" id="button_analyze" class="btn btn-outline-success btn-sm">Cutoffs Heatmap</button> -->
|
||||
<!-- <button id="button_stopall" class="btn btn-outline-success btn-sm">Stop All</button>
|
||||
<button id="button_refresh" class="btn btn-outline-success btn-sm">Refresh</button> -->
|
||||
<div id="buttons-container"></div>
|
||||
<div id="buttons-container" style="display: contents"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <div>
|
||||
@ -850,7 +851,7 @@
|
||||
<!-- tady zacina polska docasna lokalizace -->
|
||||
<!-- <script type="text/javascript" src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script> -->
|
||||
<script type="text/javascript" src="/static/js/libs/lightweightcharts/lightweight-charts.standalone.production410.js"></script>
|
||||
|
||||
<script src="/static/js/dynamicbuttons.js"></script>
|
||||
|
||||
|
||||
<!-- <script src="/static/js/utils.js?v=1.01"></script> -->
|
||||
@ -872,12 +873,11 @@
|
||||
|
||||
|
||||
|
||||
|
||||
<script src="/static/js/livewebsocket.js?v=1.01"></script>
|
||||
<script src="/static/js/realtimechart.js?v=1.01"></script>
|
||||
<script src="/static/js/mytables.js?v=1.01"></script>
|
||||
<script src="/static/js/testlist.js?v=1.01"></script>
|
||||
<script src="/static/js/configform.js?v=1.01"></script>
|
||||
<!-- <script src="/static/js/dynamicbuttons.js"></script> -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
184
v2realbot/static/index2.html
Normal file
184
v2realbot/static/index2.html
Normal file
@ -0,0 +1,184 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Bootstrap CSS (Dark Mode Enabled) -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
|
||||
<!-- Custom CSS -->
|
||||
<style>
|
||||
/* 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 {
|
||||
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; /* Spacing between form fields */
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem; /* Spacing between each form group */
|
||||
}
|
||||
/* 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; */
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-dark text-white">
|
||||
<div class="container mt-5">
|
||||
<!-- Dropdown Button -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="actionDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Choose Action
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="actionDropdown">
|
||||
<!-- Action 1-->
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
Action 1
|
||||
<i class="bi bi-play-circle float-end hover-icon"></i>
|
||||
<!-- ... Action 1 content ... -->
|
||||
<form class="d-none action-form">
|
||||
<div class="form-label-group">
|
||||
<input type="text" id="param1-action1" class="form-control form-control-sm" placeholder="Parameter 1" value="Default Value">
|
||||
<label for="param1-action1">Parameter 1</label>
|
||||
</div>
|
||||
<div class="form-label-group">
|
||||
<input type="text" id="param2-action1" class="form-control form-control-sm" placeholder="Parameter 2">
|
||||
<label for="param2-action1">Parameter 2</label>
|
||||
</div>
|
||||
<div class="form-label-group">
|
||||
<select class="form-select form-select-sm" id="select-action1">
|
||||
<option selected>Option 1</option>
|
||||
<option value="1">Option 2</option>
|
||||
<option value="2">Option 3</option>
|
||||
</select>
|
||||
<label for="select-action1">Select Option</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Submit</button>
|
||||
</form>
|
||||
</a>
|
||||
</li>
|
||||
<!-- ... Additional Actions ... -->
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">
|
||||
Action 2
|
||||
<i class="bi bi-play-circle float-end hover-icon"></i> <!-- Bootstrap Icons for Play -->
|
||||
<!-- ... Action 1 content ... -->
|
||||
<form class="d-none action-form">
|
||||
<div class="form-label-group">
|
||||
<input type="text" id="param1-action2" class="form-control form-control-sm" placeholder="Parameter 1" value="Default Value">
|
||||
<label for="param1-action1">Parameter 1</label>
|
||||
</div>
|
||||
<div class="form-label-group">
|
||||
<input type="text" id="param2-action2" class="form-control form-control-sm" placeholder="Parameter 2">
|
||||
<label for="param2-action2">Parameter 2</label>
|
||||
</div>
|
||||
<div class="form-label-group">
|
||||
<select class="form-select form-select-sm" id="select-action2">
|
||||
<option selected>Option 1</option>
|
||||
<option value="1">Option 2</option>
|
||||
<option value="2">Option 3</option>
|
||||
</select>
|
||||
<label for="select-action2">Select Option</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Submit</button>
|
||||
</form>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- jQuery and Bootstrap Bundle -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
// Toggle visibility of form on hover for any number of actions
|
||||
$('.dropdown-menu').on('mouseenter', '.dropdown-item', function() {
|
||||
$(this).find('.action-form').removeClass('d-none').show();
|
||||
});
|
||||
|
||||
$('.dropdown-menu').on('mouseleave', '.dropdown-item', function() {
|
||||
setTimeout(() => { // Timeout to prevent flickering effect
|
||||
if (!$('.action-form:hover').length) {
|
||||
$(this).find('.action-form').addClass('d-none').hide();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
});
|
||||
|
||||
|
||||
// // Show the form when hovering over the play icon
|
||||
// $('.dropdown-menu').on('mouseenter', '.hover-icon', function() {
|
||||
// $(this).siblings('.action-form').removeClass('d-none').show();
|
||||
// });
|
||||
|
||||
// // Hide the form when hovering out of the play icon and form area
|
||||
// $('.dropdown-menu').on('mouseleave', '.hover-icon, .action-form', function() {
|
||||
// setTimeout(() => { // Timeout to prevent flickering effect
|
||||
// if (!$('.action-form:hover').length) {
|
||||
// $('.action-form').hide();
|
||||
// }
|
||||
// }, 100);
|
||||
// });
|
||||
|
||||
// Hide form when mouse leaves the form area
|
||||
$('.dropdown-menu').on('mouseleave', '.action-form', function() {
|
||||
$(this).hide();
|
||||
});
|
||||
|
||||
// Submit form logic
|
||||
$('.dropdown-menu').on('submit', '.action-form', function(e) {
|
||||
e.preventDefault();
|
||||
// Add logic to process form submission
|
||||
console.log('Form submitted for', $(this).closest('.dropdown-item').text().trim());
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -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)
|
||||
});
|
||||
|
||||
@ -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")
|
||||
|
||||
buttonConfig.forEach(function(button) {
|
||||
var $btnGroup = $('<div>', {class: 'btn-group'});
|
||||
var $btn = $('<button>', {
|
||||
type: 'button',
|
||||
class: 'btn btn-primary',
|
||||
text: button.label
|
||||
// $(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);
|
||||
});
|
||||
|
||||
var $form = $('<form>', {class: 'input-group'});
|
||||
|
||||
// Handling additional parameters
|
||||
for (var key in button.additionalParameters) {
|
||||
var param = button.additionalParameters[key];
|
||||
var $input;
|
||||
|
||||
if (param.type === 'select') {
|
||||
$input = $('<select>', {class: 'form-select', name: key});
|
||||
param.options.forEach(function(option) {
|
||||
$input.append($('<option>', {value: option, text: option}));
|
||||
|
||||
|
||||
})
|
||||
|
||||
//vstupem je #some dropdown menu (TODO mozna dava smysl, abychom si element vytvorili predtim
|
||||
//a nikoliv v nasledne funkci jak to zatim je)
|
||||
function populate_dynamic_buttons(targetElement, config, batch_id = null) {
|
||||
//console.log("buttonConfig",config)
|
||||
|
||||
// Function to create form inputs based on the configuration
|
||||
function createFormInputs(additionalParameters, batch_id = null) {
|
||||
var formHtml = ''
|
||||
// else
|
||||
// {
|
||||
// $.each(runner_ids, function(index, id) {
|
||||
// formHtml += '<input type="hidden" name="runner_ids[]" value="' + id + '">';
|
||||
// });
|
||||
// }
|
||||
|
||||
$.each(additionalParameters, function(key, param) {
|
||||
// Include 'name' attribute in each input element
|
||||
var id_prefix = batch_id ? batch_id : ''
|
||||
var id = id_prefix + key
|
||||
switch(param.type) {
|
||||
case 'select':
|
||||
formHtml += '<div class="form-label-group"><select class="form-select form-select-sm" name="' + key + '" id="' + id + '">';
|
||||
$.each(param.options, function(index, option) {
|
||||
var selected = (option == param.defval) ? 'selected' : '';
|
||||
formHtml += '<option ' + selected + '>' + option + '</option>';
|
||||
});
|
||||
} else {
|
||||
$input = $('<input>', {
|
||||
type: param.type === 'number' ? 'number' : 'text',
|
||||
class: 'form-control',
|
||||
name: key,
|
||||
placeholder: key
|
||||
formHtml += '</select><label for="' + id + '">' + key + '</label></div>';
|
||||
break;
|
||||
case 'string':
|
||||
formHtml += '<div class="form-label-group"><input type="text" name="' + key + '" id="' + id + '" class="form-control form-control-sm" placeholder="' + key + '" value="' + param.default + '"><label for="' + id + '">' + key + '</label></div>';
|
||||
break;
|
||||
case 'number':
|
||||
formHtml += '<div class="form-label-group"><input type="number" name="' + key + '" id="' + id + '" class="form-control form-control-sm" placeholder="' + key + '" value="' + param.default + '"><label for="' + id + '">' + key + '</label></div>';
|
||||
break;
|
||||
case 'boolean':
|
||||
formHtml += '<div class="form-label-group"><input type="checkbox" name="' + key + '" id="' + id + '" class="form-check" ' + (param.default? 'checked' : '') + '><label for="' + id + '">' + key + '</label></div>';
|
||||
break
|
||||
|
||||
}
|
||||
});
|
||||
return formHtml;
|
||||
}
|
||||
|
||||
$form.append($input);
|
||||
//naplnime obecny element (mozna delat ve volajici fci)
|
||||
|
||||
//pro batche to je ikonka
|
||||
if (batch_id) {
|
||||
dropdownHtml = '<div class="dropdown stat_div" id="dd'+batch_id+'"><span class="material-symbols-outlined tool-icon dropdown-toggle" id="actionDropdown'+batch_id+'" data-bs-toggle="dropdown" aria-expanded="false">query_stats</span><ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="actionDropdown'+batch_id+'" id="ul'+batch_id+'"></ul></div>'
|
||||
}
|
||||
//pro runnery je to button
|
||||
else {
|
||||
dropdownHtml = '<div class="dropdown stat_div" id="dd'+batch_id+'"><button title="Available analysis to run on selected days" class="btn btn-outline-success btn-sm dropdown-toggle" type="button" id="actionDropdown'+batch_id+'" data-bs-toggle="dropdown" aria-expanded="false">Analytics</button><ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="actionDropdown'+batch_id+'" id="ul'+batch_id+'"></ul></div>'
|
||||
|
||||
$btnGroup.append($btn).append($form);
|
||||
$('#buttons-container').append($btnGroup);
|
||||
}
|
||||
targetElement.append(dropdownHtml)
|
||||
//console.log("po pridani", targetElement)
|
||||
// Find the ul element within the dropdown
|
||||
var dropdownMenu = targetElement.find('.dropdown-menu');
|
||||
|
||||
// Event listener for button
|
||||
$btn.on('click', function(event) {
|
||||
event.preventDefault();
|
||||
// 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 ? '<input type="hidden" name="batch_id" id="batch'+buttonConfig.function+batch_id+'" value="'+batch_id+'">': ''
|
||||
var buttonHtml = '<li><a class="dropdown-item" href="#">' + buttonConfig.label +
|
||||
'<i class="bi bi-play-circle float-end hover-icon"></i><form class="d-none action-form" data-endpoint="' + buttonConfig.apiEndpoint + '"><div class="spinner-border text-primary d-none" role="status" id="formSpinner"><span class="visually-hidden">Loading...</span></div><input type="hidden" name="function" id="func'+buttonConfig.function+batch_id+'" value="'+buttonConfig.function+'"></input>' +
|
||||
batchInputHtml + formHtml + '<button type="submit" class="btn btn-primary btn-sm">Submit</button></form></a></li>';
|
||||
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);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//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');
|
||||
|
||||
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: button.apiEndpoint,
|
||||
url: apiEndpoint,
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader('X-API-Key',
|
||||
API_KEY); },
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
success: function(response) {
|
||||
console.log('API Call Successful:', response);
|
||||
//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;
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('API Call Failed:', error);
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
140
v2realbot/static/js/dynamicbuttons_oldModal.js
Normal file
140
v2realbot/static/js/dynamicbuttons_oldModal.js
Normal file
@ -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 = $('<button>', {
|
||||
type: 'button',
|
||||
class: 'btn btn-primary',
|
||||
'data-bs-toggle': 'modal',
|
||||
'data-bs-target': '#' + modalId,
|
||||
text: button.label
|
||||
});
|
||||
|
||||
// Create and append modal structure
|
||||
var $modal = createModalStructure(button, modalId);
|
||||
$('#buttons-container').append($btn).append($modal);
|
||||
});
|
||||
|
||||
// Global event listener for modal form submission
|
||||
$(document).on('submit', '.modal form', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var $form = $(this);
|
||||
var formData = $form.serializeArray().reduce(function(obj, item) {
|
||||
obj[item.name] = item.value;
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
var apiEndpoint = $form.data('api-endpoint');
|
||||
|
||||
$.ajax({
|
||||
url: apiEndpoint,
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
success: function(response) {
|
||||
console.log('API Call Successful:', response);
|
||||
$form.closest('.modal').modal('hide');
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('API Call Failed:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function createModalStructure(button, modalId) {
|
||||
var $modal = $('<div>', {
|
||||
class: 'modal fade',
|
||||
id: modalId,
|
||||
tabindex: '-1',
|
||||
'aria-labelledby': modalId + 'Label',
|
||||
'aria-hidden': 'true'
|
||||
});
|
||||
|
||||
var $modalDialog = $('<div>', {class: 'modal-dialog'});
|
||||
var $modalContent = $('<div>', {class: 'modal-content'});
|
||||
|
||||
var $modalHeader = $('<div>', {class: 'modal-header'});
|
||||
$modalHeader.append($('<h5>', {
|
||||
class: 'modal-title',
|
||||
id: modalId + 'Label',
|
||||
text: button.label
|
||||
}));
|
||||
$modalHeader.append($('<button>', {
|
||||
type: 'button',
|
||||
class: 'btn-close',
|
||||
'data-bs-dismiss': 'modal',
|
||||
'aria-label': 'Close'
|
||||
}));
|
||||
|
||||
var $modalBody = $('<div>', {class: 'modal-body'});
|
||||
var $form = $('<form>', {
|
||||
'data-api-endpoint': button.apiEndpoint
|
||||
});
|
||||
|
||||
// Handling additional parameters
|
||||
for (var key in button.additionalParameters) {
|
||||
var param = button.additionalParameters[key];
|
||||
var $formGroup = $('<div>', {class: 'mb-3'});
|
||||
|
||||
if (param.type === 'select') {
|
||||
var $label = $('<label>', {class: 'form-label', text: key});
|
||||
var $select = $('<select>', {class: 'form-select', name: key});
|
||||
param.options.forEach(function(option) {
|
||||
$select.append($('<option>', {value: option, text: option}));
|
||||
});
|
||||
$formGroup.append($label).append($select);
|
||||
} else {
|
||||
$formGroup.append($('<label>', {class: 'form-label', text: key}));
|
||||
$formGroup.append($('<input>', {
|
||||
type: param.type === 'number' ? 'number' : 'text',
|
||||
class: 'form-control',
|
||||
name: key,
|
||||
placeholder: key
|
||||
}));
|
||||
}
|
||||
|
||||
$form.append($formGroup);
|
||||
}
|
||||
|
||||
var $modalFooter = $('<div>', {class: 'modal-footer'});
|
||||
$modalFooter.append($('<button>', {
|
||||
type: 'submit',
|
||||
class: 'btn btn-primary',
|
||||
text: 'Submit'
|
||||
}));
|
||||
|
||||
$modalBody.append($form);
|
||||
$modalContent.append($modalHeader).append($modalBody).append($modalFooter);
|
||||
$modalDialog.append($modalContent);
|
||||
$modal.append($modalDialog);
|
||||
|
||||
return $modal;
|
||||
}
|
||||
@ -296,7 +296,7 @@ function delete_batch(event){
|
||||
function analyze_optimal_cutoff(batch_id = null) {
|
||||
//definice parametru
|
||||
param_obj = { rem_outliers:false, steps:50}
|
||||
obj = {runner_ids:[], batch_id:null, params:param_obj}
|
||||
obj = {function: "analyze_optimal_cutoff", runner_ids:[], batch_id:null, params:param_obj}
|
||||
//bereme bud selected runners
|
||||
if (!batch_id) {
|
||||
rows = archiveRecords.rows('.selected').data();
|
||||
@ -330,6 +330,19 @@ function analyze_optimal_cutoff(batch_id = null) {
|
||||
xhrFields: {
|
||||
responseType: 'blob'
|
||||
},
|
||||
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;
|
||||
},
|
||||
contentType: "application/json",
|
||||
processData: false,
|
||||
data: JSON.stringify(obj),
|
||||
@ -344,7 +357,7 @@ function analyze_optimal_cutoff(batch_id = null) {
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.log("proc to skace do erroru?")
|
||||
//window.alert(JSON.stringify(xhr));
|
||||
window.alert(JSON.stringify(xhr));
|
||||
console.log(JSON.stringify(xhr));
|
||||
$('#button_analyze').attr('disabled',false);
|
||||
if (!batch_id) {
|
||||
@ -419,7 +432,9 @@ function display_image(imageUrl) {
|
||||
//$('#imagePreview').show();
|
||||
window.$('#imageModal').modal('show');
|
||||
};
|
||||
img.onerror = function() {
|
||||
img.onerror = function(e) {
|
||||
console.log("Image load error", e);
|
||||
console.log("Image object:", img);
|
||||
console.log("no image available")
|
||||
// If the image fails to load, do nothing
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
$(document).ready(function () {
|
||||
initialize_archiveRecords();
|
||||
archiveRecords.ajax.reload();
|
||||
disable_arch_buttons();
|
||||
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
//archive table
|
||||
var archiveRecords =
|
||||
var archiveRecords = null
|
||||
|
||||
//ekvivalent to ready
|
||||
function initialize_archiveRecords() {
|
||||
|
||||
//archive table
|
||||
archiveRecords =
|
||||
$('#archiveTable').DataTable( {
|
||||
ajax: {
|
||||
url: '/archived_runners_p/',
|
||||
@ -327,6 +332,10 @@ var archiveRecords =
|
||||
tools += '<span id="batchtool_exportcsv_button" class="material-symbols-outlined tool-icon" title="Export batch to csv">csv</span>'
|
||||
tools += '<span id="batchtool_exportxml_button" class="material-symbols-outlined tool-icon" title="Export batch to xml">insert_drive_file</span>'
|
||||
tools += '<span id="batchtool_cutoff_button" class="material-symbols-outlined tool-icon" title="Cutoff heatmap for batch">cut</span>'
|
||||
//dynamic button placeholder
|
||||
//tools += '<div class="dropdown"><button class="btn btn-outline-success btn-sm dropdown-toggle" type="button" id="actionDropdown" data-bs-toggle="dropdown" aria-expanded="false">Choose analyzer</button><ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="actionDropdown"></ul></div>'
|
||||
tools += '<div class="batch_buttons_container" id="bb'+group+'" data-batch-id="'+group+'"></div>'
|
||||
|
||||
//final closure
|
||||
tools += '</span>'
|
||||
icon_color = getColorForId(stratinId)
|
||||
@ -350,7 +359,28 @@ var archiveRecords =
|
||||
.addClass(state);
|
||||
}
|
||||
},
|
||||
// drawCallback: function (settings) {
|
||||
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();
|
||||
|
||||
@ -368,14 +398,15 @@ var archiveRecords =
|
||||
// $(rows).eq(i).show();
|
||||
// }
|
||||
|
||||
// // Set the unique identifier as a data attribute on each row
|
||||
// //$(rows).eq(i).attr('data-group-name', groupName);
|
||||
// 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');
|
||||
// // }
|
||||
// });
|
||||
// // Add or remove the 'collapsed' class based on the state
|
||||
// if (groupName.startsWith('no-batch-id-')) {
|
||||
// $('tr[data-name="' + groupName + '"]').toggleClass('collapsed', state === 'collapsed');
|
||||
// }
|
||||
});
|
||||
// });
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
@ -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; */
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user