dynamic toolbutts on json and plugin report system

This commit is contained in:
David Brazda
2023-11-30 14:11:03 +01:00
parent 648489b0f4
commit 2ecb90d83f
18 changed files with 1705 additions and 443 deletions

View File

@ -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

View File

@ -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)])

View 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()

View 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)

View 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)

View 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)

View 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)

View 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

View File

@ -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>

View 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>

View File

@ -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)
});

View File

@ -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 = $('<div>', {class: 'btn-group'});
var $btn = $('<button>', {
type: 'button',
class: 'btn btn-primary',
text: button.label
});
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}));
});
} else {
$input = $('<input>', {
type: param.type === 'number' ? 'number' : 'text',
class: 'form-control',
name: key,
placeholder: key
});
})
//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>';
});
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;
}
//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>'
}
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 ? '<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);
});
$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
// Event listener for button
$btn.on('click', function(event) {
event.preventDefault();
$submitButton.attr('disabled', true);
$spinner.removeClass('d-none');
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();
}
});
}

View 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;
}

View File

@ -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
};

View File

@ -3,6 +3,7 @@
$(document).ready(function () {
initialize_archiveRecords();
archiveRecords.ajax.reload();
disable_arch_buttons();

View File

@ -1,90 +1,72 @@
//archive table
var archiveRecords =
$('#archiveTable').DataTable( {
ajax: {
url: '/archived_runners_p/',
dataSrc: 'data',
method:"POST",
contentType: "application/json",
// dataType: "json",
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
data: function (d) {
return JSON.stringify(d);
},
error: function(xhr, status, error) {
//var err = eval("(" + xhr.responseText + ")");
//window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
}
},
columns: [{ data: 'id' },
{data: 'strat_id'},
{data: 'name'},
{data: 'symbol'},
{data: 'note'},
{data: 'started'},
{data: 'stopped'},
{data: 'mode'},
{data: 'account', visible: true},
{data: 'bt_from', visible: true},
{data: 'bt_to', visible: true},
{data: 'ilog_save', visible: true},
{data: 'profit'},
{data: 'trade_count', visible: true},
{data: 'end_positions', visible: true},
{data: 'end_positions_avgp', visible: true},
{data: 'metrics', visible: true},
{data: 'batch_id', visible: true},
],
paging: true,
processing: true,
serverSide: true,
columnDefs: [
{
targets: 1,
render: function ( data, type, row ) {
if (type === 'display') {
//console.log("arch")
var color = getColorForId(data);
return '<div class="tdnowrap" data-bs-toggle="tooltip" data-bs-placement="top" title="'+data+'"><span class="color-tag" style="background-color:' + color + ';"></span>'+data+'</div>';
}
return data;
},
},
{
targets: [0,17],
render: function ( data, type, row ) {
if (!data) return data
return '<div class="tdnowrap" title="'+data+'">'+data+'</i>'
},
},
{
targets: [5],
render: function ( data, type, row ) {
now = new Date(data)
if (type == "sort") {
return new Date(data).getTime();
}
var date = new Date(data);
tit = date.toLocaleString('cs-CZ', {
timeZone: 'America/New_York',
})
var archiveRecords = null
if (isToday(now)) {
//return local time only
return '<div title="'+tit+'">'+ 'dnes ' + format_date(data,false,true)+'</div>'
}
else
{
//return local datetime
return '<div title="'+tit+'">'+ format_date(data,false,false)+'</div>'
}
//ekvivalent to ready
function initialize_archiveRecords() {
//archive table
archiveRecords =
$('#archiveTable').DataTable( {
ajax: {
url: '/archived_runners_p/',
dataSrc: 'data',
method:"POST",
contentType: "application/json",
// dataType: "json",
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
data: function (d) {
return JSON.stringify(d);
},
error: function(xhr, status, error) {
//var err = eval("(" + xhr.responseText + ")");
//window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
}
},
columns: [{ data: 'id' },
{data: 'strat_id'},
{data: 'name'},
{data: 'symbol'},
{data: 'note'},
{data: 'started'},
{data: 'stopped'},
{data: 'mode'},
{data: 'account', visible: true},
{data: 'bt_from', visible: true},
{data: 'bt_to', visible: true},
{data: 'ilog_save', visible: true},
{data: 'profit'},
{data: 'trade_count', visible: true},
{data: 'end_positions', visible: true},
{data: 'end_positions_avgp', visible: true},
{data: 'metrics', visible: true},
{data: 'batch_id', visible: true},
],
paging: true,
processing: true,
serverSide: true,
columnDefs: [
{
targets: 1,
render: function ( data, type, row ) {
if (type === 'display') {
//console.log("arch")
var color = getColorForId(data);
return '<div class="tdnowrap" data-bs-toggle="tooltip" data-bs-placement="top" title="'+data+'"><span class="color-tag" style="background-color:' + color + ';"></span>'+data+'</div>';
}
return data;
},
},
{
targets: [0,17],
render: function ( data, type, row ) {
if (!data) return data
return '<div class="tdnowrap" title="'+data+'">'+data+'</i>'
},
},
{
targets: [6],
targets: [5],
render: function ( data, type, row ) {
now = new Date(data)
if (type == "sort") {
@ -93,289 +75,338 @@ var archiveRecords =
var date = new Date(data);
tit = date.toLocaleString('cs-CZ', {
timeZone: 'America/New_York',
})
})
if (isToday(now)) {
//return local time only
return '<div title="'+tit+'" class="token level comment">'+ 'dnes ' + format_date(data,false,true)+'</div>'
return '<div title="'+tit+'">'+ 'dnes ' + format_date(data,false,true)+'</div>'
}
else
{
//return local datetime
return '<div title="'+tit+'" class="token level number">'+ format_date(data,false,false)+'</div>'
return '<div title="'+tit+'">'+ format_date(data,false,false)+'</div>'
}
},
},
{
targets: [9,10],
render: function ( data, type, row ) {
if (type == "sort") {
return new Date(data).getTime();
}
//console.log(data)
//market datetime
return data ? format_date(data, true) : data
},
},
{
targets: [2],
render: function ( data, type, row ) {
return '<div class="tdname tdnowrap" title="'+data+'">'+data+'</div>'
},
},
// {
// targets: [4],
// render: function ( data, type, row ) {
// return '<div class="tdname tdnowrap" title="'+data+'">'+data+'</div>'
// },
// },
{
targets: [16],
render: function ( data, type, row ) {
//console.log("metrics", data)
try {
data = JSON.parse(data)
}
catch (error) {
//console.log(error)
}
var res = JSON.stringify(data)
var unquoted = res.replace(/"([^"]+)":/g, '$1:')
{
targets: [6],
render: function ( data, type, row ) {
now = new Date(data)
if (type == "sort") {
return new Date(data).getTime();
}
var date = new Date(data);
tit = date.toLocaleString('cs-CZ', {
timeZone: 'America/New_York',
})
//zobrazujeme jen kratkou summary pokud mame, jinak davame vse, do titlu davame vzdy vse
if (isToday(now)) {
//return local time only
return '<div title="'+tit+'" class="token level comment">'+ 'dnes ' + format_date(data,false,true)+'</div>'
}
else
{
//return local datetime
return '<div title="'+tit+'" class="token level number">'+ format_date(data,false,false)+'</div>'
}
},
},
{
targets: [9,10],
render: function ( data, type, row ) {
if (type == "sort") {
return new Date(data).getTime();
}
//console.log(data)
short = null
if ((data) && (data.profit) && (data.profit.sum)) {
short = data.profit.sum
//market datetime
return data ? format_date(data, true) : data
},
},
{
targets: [2],
render: function ( data, type, row ) {
return '<div class="tdname tdnowrap" title="'+data+'">'+data+'</div>'
},
},
// {
// targets: [4],
// render: function ( data, type, row ) {
// return '<div class="tdname tdnowrap" title="'+data+'">'+data+'</div>'
// },
// },
{
targets: [16],
render: function ( data, type, row ) {
//console.log("metrics", data)
try {
data = JSON.parse(data)
}
catch (error) {
//console.log(error)
}
var res = JSON.stringify(data)
var unquoted = res.replace(/"([^"]+)":/g, '$1:')
//zobrazujeme jen kratkou summary pokud mame, jinak davame vse, do titlu davame vzdy vse
//console.log(data)
short = null
if ((data) && (data.profit) && (data.profit.sum)) {
short = data.profit.sum
}
else {
short = unquoted
}
return '<div class="tdmetrics" title="'+unquoted+'">'+short+'</div>'
},
},
{
targets: [4],
render: function ( data, type, row ) {
return '<div class="tdnote" title="'+data+'">'+data+'</div>'
},
},
{
targets: [13,14,15],
render: function ( data, type, row ) {
return '<div class="tdsmall">'+data+'</div>'
},
},
{
targets: [11],
render: function ( data, type, row ) {
//if ilog_save true
if (data) {
return '<span class="material-symbols-outlined">done_outline</span>'
}
else {
return null
}
},
},
{
targets: [8],
render: function ( data, type, row ) {
//if ilog_save true
if (data == "ACCOUNT1") {
res="ACC1"
}
else if (data == "ACCOUNT2") {
res="ACC2"
}
else { res=data}
return res
},
},
{
targets: [7],
render: function ( data, type, row ) {
//if ilog_save true
if (data == "backtest") {
res="bt"
}
else { res=data}
return res
},
}
],
order: [[6, 'desc']],
select: {
info: true,
style: 'multi',
//selector: 'tbody > tr:not(.group-header)'
selector: 'tbody > tr:not(.group-header)'
},
paging: true,
// lengthChange: false,
// select: true,
// createdRow: function( row, data, dataIndex){
// if (is_running(data.id) ){
// alert("runner");
// $(row).addClass('highlight');
// }
//}
// Add row grouping based on 'batch_id'
//TODO projit a zrevidovat - pripadne optimalizovat
//NOTE zde jse skoncil
rowGroup: {
dataSrc: 'batch_id',
//toto je volano pri renderovani headeru grupy
startRender: function (rows, group) {
var firstRowData = rows.data()[0];
//pro no-batch-id je idcko prvni id
var groupId = group ? group : 'no-batch-id-' + firstRowData.id;
var stateKey = 'dt-group-state-' + groupId;
var state = localStorage.getItem(stateKey);
// Iterate over each row in the group to set the data attribute
// zaroven pro kazdy node nastavime viditelnost podle nastaveni
rows.every(function (rowIdx, tableLoop, rowLoop) {
var rowNode = $(this.node());
rowNode.attr('data-group-name', groupId);
if (state == 'collapsed') {
rowNode.hide();
} else {
rowNode.show();
}
});
// Initialize variables for the group
var itemCount = 0;
var period = '';
var profit = '';
var started = null;
var stratinId = null;
// // Process each item only once
// archiveRecords.rows({ search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop) {
// var data = this.data();
// if ((group && data.batch_id == group)) {
// itemCount++;
// if (itemCount === 1 ) {
// firstNote = data.note ? data.note.substring(0, 14) : '';
// 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)
}
//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 {
short = unquoted
profit = existingBatch.profit
itemCount = existingBatch.itemCount
period = existingBatch.period
started = existingBatch.started
stratinId = existingBatch.stratinId
}
return '<div class="tdmetrics" title="'+unquoted+'">'+short+'</div>'
},
},
{
targets: [4],
render: function ( data, type, row ) {
return '<div class="tdnote" title="'+data+'">'+data+'</div>'
},
},
{
targets: [13,14,15],
render: function ( data, type, row ) {
return '<div class="tdsmall">'+data+'</div>'
},
},
{
targets: [11],
render: function ( data, type, row ) {
//if ilog_save true
if (data) {
return '<span class="material-symbols-outlined">done_outline</span>'
}
else {
return null
}
},
},
{
targets: [8],
render: function ( data, type, row ) {
//if ilog_save true
if (data == "ACCOUNT1") {
res="ACC1"
}
else if (data == "ACCOUNT2") {
res="ACC2"
}
else { res=data}
return res
},
},
{
targets: [7],
render: function ( data, type, row ) {
//if ilog_save true
if (data == "backtest") {
res="bt"
}
else { res=data}
return res
},
}
],
order: [[6, 'desc']],
select: {
info: true,
style: 'multi',
//selector: 'tbody > tr:not(.group-header)'
selector: 'tbody > tr:not(.group-header)'
},
paging: true,
// lengthChange: false,
// select: true,
// createdRow: function( row, data, dataIndex){
// if (is_running(data.id) ){
// alert("runner");
// $(row).addClass('highlight');
// }
//}
// Add row grouping based on 'batch_id'
//TODO projit a zrevidovat - pripadne optimalizovat
//NOTE zde jse skoncil
rowGroup: {
dataSrc: 'batch_id',
//toto je volano pri renderovani headeru grupy
startRender: function (rows, group) {
var firstRowData = rows.data()[0];
//pro no-batch-id je idcko prvni id
var groupId = group ? group : 'no-batch-id-' + firstRowData.id;
var stateKey = 'dt-group-state-' + groupId;
var state = localStorage.getItem(stateKey);
// Iterate over each row in the group to set the data attribute
// zaroven pro kazdy node nastavime viditelnost podle nastaveni
rows.every(function (rowIdx, tableLoop, rowLoop) {
var rowNode = $(this.node());
rowNode.attr('data-group-name', groupId);
if (state == 'collapsed') {
rowNode.hide();
} else {
rowNode.show();
}
});
// Initialize variables for the group
var itemCount = 0;
var period = '';
var profit = '';
var started = null;
var stratinId = null;
//zaroven nastavime u vsech childu
// // Process each item only once
// archiveRecords.rows({ search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop) {
// var data = this.data();
// Construct the GROUP HEADER - sem pripadna tlačítka atp.
//var groupHeaderContent = '<strong>' + (group ? 'Batch ID: ' + group : 'No Batch') + '</strong>';
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 = '<span class="batchtool">'
tools += '<span id="batchtool_report_button" class="material-symbols-outlined tool-icon" title="Batch Report">lab_profile</span>'
tools += '<span id="batchtool_delete_button" class="material-symbols-outlined tool-icon" title="Delete Batch">delete</span>'
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>'
// if ((group && data.batch_id == group)) {
// itemCount++;
// if (itemCount === 1 ) {
// firstNote = data.note ? data.note.substring(0, 14) : '';
// 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 += '</span>'
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 = '<span class="material-symbols-outlined expand-icon" style="background-color:' + icon_color + ';" title="Expand">'+exp_coll_icon_name+'</span>'
//zaroven nastavime u vsech childu
// Construct the GROUP HEADER - sem pripadna tlačítka atp.
//var groupHeaderContent = '<strong>' + (group ? 'Batch ID: ' + group : 'No Batch') + '</strong>';
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 = '<span class="batchtool">'
tools += '<span id="batchtool_report_button" class="material-symbols-outlined tool-icon" title="Batch Report">lab_profile</span>'
tools += '<span id="batchtool_delete_button" class="material-symbols-outlined tool-icon" title="Delete Batch">delete</span>'
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>'
//final closure
tools += '</span>'
icon_color = getColorForId(stratinId)
profit_icon_color = (profit>0) ? "#4f8966" : "#bb2f5e" //"#d42962"
//console.log(group, groupId, stratinId)
//var groupHeaderContent = '<span class="batchheader-batch-id">'+(group ? '<span class="color-tag" style="background-color:' + getColorForId(stratinId) + ';"></span>Batch ID: ' + group: 'No Batch')+'</span>';
var groupHeaderContent = '<span class="batchheader-batch-id">'+ icon + (group ? 'Batch ID: ' + group: 'No Batch')+'</span>';
groupHeaderContent += (group ? ' <span class="batchheader-count-info">(' + itemCount + ')</span>' + ' <span class="batchheader-period-info">' + period + '</span> <span class="batchheader-profit-info" style="color:'+profit_icon_color+'">Profit: ' + profit + '</span>' : '');
groupHeaderContent += group ? tools : ""
return $('<tr/>')
.append('<td colspan="18">' + groupHeaderContent + '</td>')
.attr('data-name', groupId)
.addClass('group-header')
.addClass(state);
}
else {
//def color for no batch - semi transparent
icon_color = "#ced4da17"
}
icon = '<span class="material-symbols-outlined expand-icon" style="background-color:' + icon_color + ';" title="Expand">'+exp_coll_icon_name+'</span>'
},
drawCallback: function (settings) {
//console.log("drawcallback", configData)
setTimeout(function(){
//console.log(group, groupId, stratinId)
//var groupHeaderContent = '<span class="batchheader-batch-id">'+(group ? '<span class="color-tag" style="background-color:' + getColorForId(stratinId) + ';"></span>Batch ID: ' + group: 'No Batch')+'</span>';
var groupHeaderContent = '<span class="batchheader-batch-id">'+ icon + (group ? 'Batch ID: ' + group: 'No Batch')+'</span>';
groupHeaderContent += (group ? ' <span class="batchheader-count-info">(' + itemCount + ')</span>' + ' <span class="batchheader-period-info">' + period + '</span> <span class="batchheader-profit-info" style="color:'+profit_icon_color+'">Profit: ' + profit + '</span>' : '');
groupHeaderContent += group ? tools : ""
return $('<tr/>')
.append('<td colspan="18">' + groupHeaderContent + '</td>')
.attr('data-name', groupId)
.addClass('group-header')
.addClass(state);
//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');
// // }
// });
// }
});
});
}

View File

@ -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; */
}