13 Commits

26 changed files with 2962 additions and 1486 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):
@ -52,6 +51,15 @@ class DataTablesRequest(BaseModel):
# return user.id
# raise HTTPException(status_code=404, detail=f"Could not find user with id: {id}")
#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
params: Optional[dict] = {}
class RunDay(BaseModel):
"""
Helper object for batch run - carries list of days in format required by run batch manager

View File

@ -134,13 +134,21 @@ ACCOUNT1_LIVE_MAX_BATCH_SIZE = 1
ACCOUNT1_LIVE_PAPER = False
ACCOUNT1_LIVE_FEED = DataFeed.SIP
#SECONDARY PAPER
ACCOUNT2_PAPER_API_KEY = 'PK0OQHZG03PUZ1SC560V'
ACCOUNT2_PAPER_SECRET_KEY = 'cTglhm7kwRcZfFT27fQWz31sXaxadzQApFDW6Lat'
#SECONDARY PAPER - Martin
ACCOUNT2_PAPER_API_KEY = 'PKPDTCQLNHCBC2D9GQFB'
ACCOUNT2_PAPER_SECRET_KEY = 'c1Z2V0gBleQmwHYCreqqTs45Jy33RqPGrofuSayz'
ACCOUNT2_PAPER_MAX_BATCH_SIZE = 1
ACCOUNT2_PAPER_PAPER = True
ACCOUNT2_PAPER_FEED = DataFeed.IEX
# #SECONDARY PAPER
# ACCOUNT2_PAPER_API_KEY = 'PK0OQHZG03PUZ1SC560V'
# ACCOUNT2_PAPER_SECRET_KEY = 'cTglhm7kwRcZfFT27fQWz31sXaxadzQApFDW6Lat'
# ACCOUNT2_PAPER_MAX_BATCH_SIZE = 1
# ACCOUNT2_PAPER_PAPER = True
# ACCOUNT2_PAPER_FEED = DataFeed.IEX
class KW:
activate: str = "activate"
dont_go: str = "dont_go"

View File

@ -11,7 +11,7 @@ import uvicorn
from uuid import UUID
import v2realbot.controller.services as cs
from v2realbot.utils.ilog import get_log_window
from v2realbot.common.model import StrategyInstance, RunnerView, RunRequest, Trade, RunArchive, RunArchiveView, RunArchiveViewPagination, RunArchiveDetail, Bar, RunArchiveChange, TestList, ConfigItem, InstantIndicator, DataTablesRequest
from v2realbot.common.model import StrategyInstance, RunnerView, RunRequest, Trade, RunArchive, RunArchiveView, RunArchiveViewPagination, RunArchiveDetail, Bar, RunArchiveChange, TestList, ConfigItem, InstantIndicator, DataTablesRequest, AnalyzerInputs
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException, status, WebSocketException, Cookie, Query
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
@ -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()
@ -590,16 +591,38 @@ 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/{batch_id}", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"image/png": {}}}})
def _generate_analysis(batch_id: str):
@app.post("/batches/optimizecutoff", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"image/png": {}}}})
def _optimize_cutoff(analyzerInputs: AnalyzerInputs):
try:
res, stream = find_optimal_cutoff(batch_id=batch_id, steps=50, stream=True)
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 = 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

@ -10,6 +10,7 @@ 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
@ -211,7 +212,7 @@ def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: b
plt.yticks(rotation=0) # Keep y-axis labels horizontal
plt.gca().invert_yaxis()
plt.gca().invert_xaxis()
plt.suptitle("Total Profit for Combinations of Profit and Loss Cutoffs", fontsize=16)
plt.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")
@ -235,6 +236,7 @@ if __name__ == '__main__':
# id_list = ["e8938b2e-8462-441a-8a82-d823c6a025cb"]
# generate_trading_report_image(runner_ids=id_list)
batch_id = "c76b4414"
vstup = AnalyzerInputs(**params)
res, val = find_optimal_cutoff(batch_id=batch_id, file="optimal_cutoff_vectorized.png",steps=20)
#res, val = find_optimal_cutoff(batch_id=batch_id, rem_outliers=True, file="optimal_cutoff_vectorized_nooutliers.png")

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

@ -57,7 +57,7 @@
<!-- <script src="https://code.jquery.com/jquery-3.5.1.js"></script> -->
<link rel="stylesheet" href="/static/main.css">
<link rel="stylesheet" href="/static/main.css?v=1.05">
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/mousetrap/1.4.6/mousetrap.min.js"></script> -->
<script src="/static/js/libs/mousetrap.min.js"></script>
@ -309,21 +309,22 @@
<div class="legend" id="legendArchive"></div>
</div> -->
<div id="controls">
<button id="button_edit_arch" class="btn btn-outline-success btn-sm">Edit(a)</button>
<button id="button_delete_arch" class="btn btn-outline-success btn-sm">Delete(d)</button>
<button id="button_delete_batch" class="btn btn-outline-success btn-sm">Delete Batch(b)</button>
<button id="button_show_arch" class="btn btn-outline-success btn-sm">Show(w)</button>
<button title="Edit selected days" id="button_edit_arch" class="btn btn-outline-success btn-sm">Edit(a)</button>
<button title="Delete selected days" id="button_delete_arch" class="btn btn-outline-success btn-sm">Delete(d)</button>
<!-- <button id="button_delete_batch" class="btn btn-outline-success btn-sm">Delete Batch(b)</button> -->
<button title="Show selected day on the chart" id="button_show_arch" class="btn btn-outline-success btn-sm">Show(w)</button>
<button id="button_refresh" class="refresh btn btn-outline-success btn-sm">Refresh</button>
<button id="button_compare_arch" class="refresh btn btn-outline-success btn-sm">Compare</button>
<button id="button_runagain_arch" class="refresh btn btn-outline-success btn-sm">Run Again(r)</button>
<button id="button_selpage" class="btn btn-outline-success btn-sm">Select all</button>
<button id="button_export_xml" class="btn btn-outline-success btn-sm">Export xml</button>
<button id="button_export_csv" class="btn btn-outline-success btn-sm">Export csv</button>
<button title="Compare selected days" id="button_compare_arch" class="refresh btn btn-outline-success btn-sm">Compare</button>
<button title="Run selected day" id="button_runagain_arch" class="refresh btn btn-outline-success btn-sm">Run Again(r)</button>
<button title="Select all days on the page" id="button_selpage" class="btn btn-outline-success btn-sm">Select all</button>
<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 batch creates heatmap for optimal profit/loss cutoffs" id="button_analyze" class="btn btn-outline-success btn-sm">Optimal cutoffs</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,18 +851,33 @@
<!-- 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"></script>
<script src="/static/js/instantindicators.js"></script>
<script src="/static/js/archivechart.js"></script>
<script src="/static/js/archivetables.js"></script>
<script src="/static/js/livewebsocket.js"></script>
<script src="/static/js/realtimechart.js"></script>
<script src="/static/js/mytables.js"></script>
<script src="/static/js/testlist.js"></script>
<script src="/static/js/configform.js"></script>
<!-- <script src="/static/js/dynamicbuttons.js"></script> -->
<!-- <script src="/static/js/utils.js?v=1.01"></script> -->
<!-- new util structure and exports and colors -->
<script src="/static/js/utils/utils.js?v=1.01"></script>
<script src="/static/js/utils/exports.js?v=1.01"></script>
<script src="/static/js/utils/colors.js?v=1.01"></script>
<script src="/static/js/instantindicators.js?v=1.01"></script>
<script src="/static/js/archivechart.js?v=1.03"></script>
<!-- <script src="/static/js/archivetables.js?v=1.05"></script> -->
<!-- archiveTables split into separate files -->
<script src="/static/js/tables/archivetable/init.js?v=1.03"></script>
<script src="/static/js/tables/archivetable/functions.js?v=1.03"></script>
<script src="/static/js/tables/archivetable/modals.js?v=1.03"></script>
<script src="/static/js/tables/archivetable/handlers.js?v=1.03"></script>
<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>
</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>

File diff suppressed because it is too large Load Diff

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
$submitButton.attr('disabled', true);
$spinner.removeClass('d-none');
// Event listener for button
$btn.on('click', function(event) {
event.preventDefault();
var formData = $form.serializeArray().reduce(function(obj, item) {
obj[item.name] = item.value;
return obj;
}, {});
$.ajax({
url: button.apiEndpoint,
method: 'POST',
data: formData,
success: function(response) {
console.log('API Call Successful:', response);
},
error: function(error) {
console.error('API Call Failed:', error);
console.log("toto jsou transformovana data", obj)
var apiEndpoint = $(this).data('endpoint');
// console.log("formdata", formData)
// API call (adjust as needed for your backend)
$.ajax({
url: apiEndpoint,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method: 'POST',
//menime hlavicku podle toho jestli je uspesne nebo ne, abychom mohli precist chybovou hlasku
xhr: function() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 2) { // Headers have been received
if (xhr.status === 200) {
xhr.responseType = "blob"; // Set responseType to 'blob' for successful image responses
} else {
xhr.responseType = "text"; // Set responseType to 'text' for error messages
}
}
};
return xhr;
},
xhrFields: {
responseType: 'blob'
},
contentType: "application/json",
processData: false,
data: JSON.stringify(obj),
success: function(data, textStatus, xhr) {
if (xhr.getResponseHeader("Content-Type") === "image/png") {
// Process as Blob
var blob = new Blob([data], { type: 'image/png' });
var url = window.URL || window.webkitURL;
display_image(url.createObjectURL(blob));
} else {
// Process as JSON
console.log('Received JSON', data);
}
});
$submitButton.attr('disabled', false);
$spinner.addClass('d-none');
},
error: function(xhr, status, error) {
$spinner.addClass('d-none');
$submitButton.attr('disabled', false);
console.log(xhr, status, error)
console.log(xhr.responseJSON.message)
if (xhr.responseJSON && xhr.responseJSON.detail) {
console.log('Error:', xhr.responseJSON.detail);
window.alert(xhr.responseJSON.detail);
} else {
// Fallback error message
console.log('Error:', error);
window.alert('An unexpected error occurred');
}
}
});
console.log('Form submitted for', $(this).closest('.dropdown-item').text().trim());
});
});
//HANDLERS
//CLICKABLE VERSION (odstranit d-none z action-formu)
// Attach click event to each dropdown item
// $('.dropdown-menu').on('click', '.dropdown-item', function(event) {
// event.stopPropagation(); // Stop the event from bubbling up
// var currentForm = $(this).find('.action-form');
// // Hide all other forms
// $('.action-form').not(currentForm).hide();
// // Toggle current form
// currentForm.toggle();
// });
// // Hide form when clicking outside
// $(document).on('click', function(event) {
// if (!$(event.target).closest('.dropdown-item').length) {
// $('.action-form').hide();
// }
// });
// // Prevent global click event from hiding form when clicking inside a form
// $('.dropdown-menu').on('click', '.action-form', function(event) {
// event.stopPropagation();
// });
//VERZE on HOVER (je treba pridat class d-none do action formu)
// Toggle visibility of form on hover
targetElement.find('.dropdown-menu').on('mouseenter', '.dropdown-item', function() {
$(this).find('.action-form').removeClass('d-none').show();
}).on('mouseleave', '.dropdown-item', function() {
setTimeout(() => {
if (!$('.action-form:hover').length) {
$(this).find('.action-form').addClass('d-none').hide();
}
}, 50);
});
// // Hide form when mouse leaves the form area
// targetElement.find('.dropdown-menu').on('mouseleave', '.action-form', function() {
// $(this).hide();
// });
// stop propagating click up
targetElement.find('.dropdown').on('click', function(event) {
// Stop the event from propagating to parent elements
event.stopPropagation();
});
// stop propagating click up
targetElement.find('.action-form').on('click', function(event) {
// Stop the event from propagating to parent elements
event.stopPropagation();
// Check if the clicked element or any of its parents is a submit button
if (!$(event.target).closest('input[type="submit"], button[type="submit"]').length) {
// Stop the event from propagating to parent elements
event.preventDefault();
}
});
}

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

File diff suppressed because one or more lines are too long

View File

@ -302,6 +302,7 @@ $(document).ready(function () {
runnerRecords.ajax.reload();
stratinRecords.ajax.reload();
archiveRecords.ajax.reload();
disable_arch_buttons();
})
//button copy
@ -927,11 +928,23 @@ var runnerRecords =
],
paging: false,
processing: false,
columnDefs: [ {
targets: [0,1],
columnDefs: [
{
targets: [0],
render: function ( data, type, row ) {
return '<div class="tdnowrap" title="'+data+'">'+data+'</i>'
},
},
{
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: [2],

View File

@ -0,0 +1,550 @@
//funkce a promenne specificke pro archiveTable
//usually work with archiveRecords
//ARCHIVE TABLES
let editor_diff_arch1
let editor_diff_arch2
var archData = null
var batchHeaders = []
function refresh_arch_and_callback(row, callback) {
//console.log("entering refresh")
var request = $.ajax({
url: "/archived_runners/"+row.id,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"GET",
contentType: "application/json",
dataType: "json",
success:function(data){
//console.log("fetched data ok")
//console.log(JSON.stringify(data,null,2));
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
}
});
// Handling the responses of both requests
$.when(request).then(function(response) {
// Both requests have completed successfully
//console.log("Result from request:", response);
//console.log("Response received. calling callback")
//call callback function
callback(response)
}, function(error) {
// Handle errors from either request here
// Example:
console.error("Error from first request:", error);
console.log("requesting id error")
});
}
//triggers charting
function get_detail_and_chart(row) {
$.ajax({
url:"/archived_runners_detail/"+row.id,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"GET",
contentType: "application/json",
dataType: "json",
success:function(data){
$('#button_show_arch').attr('disabled',false);
$('#chartContainerInner').addClass("show");
//$("#lines").html("<pre>"+JSON.stringify(row.stratvars,null,2)+"</pre>")
//$('#chartArchive').append(JSON.stringify(data,null,2));
//console.log(JSON.stringify(data,null,2));
//if lower res is required call prepare_data otherwise call chart_archived_run()
//get other base resolutions
prepare_data(row, 1, "Min", data)
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
//console.log(JSON.stringify(xhr));
$('#button_show_arch').attr('disabled',false);
}
})
}
//rerun stratin
function run_day_again() {
row = archiveRecords.row('.selected').data();
$('#button_runagain_arch').attr('disabled',true);
var record1 = new Object()
//console.log(JSON.stringify(rows))
//record1 = JSON.parse(rows[0].strat_json)
//record1.json = rows[0].json
//TBD mozna zkopirovat jen urcite?
//getting required data (detail of the archived runner + stratin to be run)
var request1 = $.ajax({
url: "/archived_runners/"+row.id,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"GET",
contentType: "application/json",
dataType: "json",
success:function(data){
//console.log("fetched data ok")
//console.log(JSON.stringify(data,null,2));
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
}
});
//nalaodovat data pro strategii
var request2 = $.ajax({
url: "/stratins/"+row.strat_id,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"GET",
contentType: "application/json",
dataType: "json",
success:function(data){
//console.log("fetched data ok")
//console.log(JSON.stringify(data,null,2));
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
}
});
// Handling the responses of both requests
$.when(request1, request2).then(function(response1, response2) {
// Both requests have completed successfully
var result1 = response1[0];
var result2 = response2[0];
//console.log("Result from first request:", result1);
//console.log("Result from second request:", result2);
//console.log("calling compare")
rerun_strategy(result1, result2)
// Perform your action with the results from both requests
// Example:
}, function(error1, error2) {
// Handle errors from either request here
// Example:
console.error("Error from first request:", error1);
console.error("Error from second request:", error2);
});
function rerun_strategy(archRunner, stratData) {
record1 = archRunner
//console.log(record1)
//smazeneme nepotrebne a pridame potrebne
//do budoucna predelat na vytvoreni noveho objektu
//nebudeme muset odstanovat pri kazdem pridani noveho atributu v budoucnu
delete record1["end_positions"];
delete record1["end_positions_avgp"];
delete record1["profit"];
delete record1["trade_count"];
delete record1["stratvars_toml"];
delete record1["started"];
delete record1["stopped"];
delete record1["metrics"];
delete record1["settings"];
delete record1["stratvars"];
record1.note = "RERUN " + record1.note
if (record1.bt_from == "") {delete record1["bt_from"];}
if (record1.bt_to == "") {delete record1["bt_to"];}
//mazeme, pouze rerunujeme single
delete record1["test_batch_id"];
delete record1["batch_id"];
const rec = new Object()
rec.id2 = parseInt(stratData.id2);
rec.name = stratData.name;
rec.symbol = stratData.symbol;
rec.class_name = stratData.class_name;
rec.script = stratData.script;
rec.open_rush = stratData.open_rush;
rec.close_rush = stratData.close_rush;
rec.stratvars_conf = stratData.stratvars_conf;
rec.add_data_conf = stratData.add_data_conf;
rec.note = stratData.note;
rec.history = "";
strat_json = JSON.stringify(rec, null, 2);
record1.strat_json = strat_json
//zkopirujeme strat_id do id a smazeme strat_id
record1.id = record1.strat_id
delete record1["strat_id"];
//console.log("record1 pred odeslanim", record1)
jsonString = JSON.stringify(record1);
$.ajax({
url:"/stratins/"+record1.id+"/run",
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"PUT",
contentType: "application/json",
data: jsonString,
success:function(data){
$('#button_runagain_arch').attr('disabled',false);
setTimeout(function () {
runnerRecords.ajax.reload();
stratinRecords.ajax.reload();
}, 1500);
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
//console.log(JSON.stringify(xhr));
$('#button_runagain_arch').attr('disabled',false);
}
})
}
}
function expand_collapse_rows(event) {
event.stopPropagation()
var headerRow = $(this);
var name = headerRow.data('name');
var collapsed = headerRow.hasClass('collapsed');
// Toggle the expand icon name
var expandIcon = headerRow.find('.expand-icon');
if (collapsed) {
expandIcon.text('expand_less');
} else {
expandIcon.text('expand_more');
}
headerRow.toggleClass('collapsed');
archiveRecords.rows().every(function () {
var row = $(this.node());
var rowGroup = row.attr('data-group-name');
if (rowGroup == name) {
row.toggle();
}
});
// Save the state
if (collapsed) {
localStorage.setItem('dt-group-state-' + name, 'expanded');
} else {
localStorage.setItem('dt-group-state-' + name, 'collapsed');
}
}
function delete_batch(event){
event.preventDefault();
batch_id = $('#batch_id_del').val();
$('#deletebatch').attr('disabled', 'disabled');
$.ajax({
url:"/archived_runners/batch/"+batch_id,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"DELETE",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(batch_id),
success:function(data){
$('#delFormBatch')[0].reset();
window.$('#delModalBatch').modal('hide');
$('#deletebatch').attr('disabled', false);
$('#button_delete_batch').attr('disabled', false);
//console.log(data)
archiveRecords.ajax.reload();
disable_arch_buttons();
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
$('#deletebatch').attr('disabled', false);
$('#button_delete_batch').attr('disabled', false);
archiveRecords.ajax.reload();
disable_arch_buttons();
}
})
}
function analyze_optimal_cutoff(batch_id = null) {
//definice parametru
param_obj = { rem_outliers:false, steps:50}
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();
if (rows == undefined) {
return
}
$('#button_analyze').attr('disabled','disabled');
// Extract IDs from each row's data and store them in an array
obj.runner_ids = [];
for (var i = 0; i < rows.length; i++) {
obj.runner_ids.push(rows[i].id); // Assuming 'id' is the property that contains the row ID
}
}
//nebo batch
else {
obj.batch_id = batch_id
}
console.log("analyze cutoff objekt", obj)
// batch_id: Optional[str] = None
// runner_ids: Optional[List[UUID]] = None
// #additional parameter
// params: Optional[dict] = None
$.ajax({
url:"/batches/optimizecutoff/",
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"POST",
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),
success:function(blob){
var url = window.URL || window.webkitURL;
console.log("vraceny obraz", blob)
console.log("url",url.createObjectURL(blob))
display_image(url.createObjectURL(blob))
if (!batch_id) {
$('#button_analyze').attr('disabled',false);
}
},
error: function(xhr, status, error) {
console.log("proc to skace do erroru?")
window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
$('#button_analyze').attr('disabled',false);
if (!batch_id) {
$('#button_analyze').attr('disabled',false);
}
}
})
}
//pomocna funkce, ktera vraci filtrovane radky tabulky (bud oznacene nebo batchove)
function get_selected_or_batch(batch_id = null) {
if (!batch_id) {
rows = archiveRecords.rows('.selected');
} else {
rows = archiveRecords.rows( function ( idx, data, node ) {
return data.batch_id == batch_id;
});
//console.log("batch rows",batch_id, rows)
}
}
//prepares export data, either for selected rows or based on batch_id
function prepare_export(batch_id = null) {
rows = get_selected_or_batch(batch_id)
var trdList = []
if(rows.data().length > 0 ) {
//console.log(rows.data())
// Loop through the selected rows and display an alert with each row's ID
rows.every(function (rowIdx, tableLoop, rowLoop ) {
var data = this.data()
data.metrics.prescr_trades.forEach((trade) => {
new_obj = {}
new_obj["entry_time"] = (trade.entry_time) ? new Date(trade.entry_time * 1000) : null
new_obj["entry_time"] = (new_obj["entry_time"]) ? new_obj["entry_time"].toLocaleString('cs-CZ', {
timeZone: 'America/New_York',
}) : null
new_obj["exit_time"] = (trade.exit_time) ? new Date(trade.exit_time * 1000):null
new_obj["exit_time"] = (new_obj["exit_time"]) ? new_obj["exit_time"].toLocaleString('cs-CZ', {
timeZone: 'America/New_York',
}) : null
new_obj["direction"] = trade.direction
new_obj["profit"] = trade.profit
new_obj["rel_profit"] = trade.rel_profit
trdList.push(new_obj)
})
});
}
return trdList
}
function download_exported_data(type, batch_id = null) {
filename = batch_id ? "batch"+batch_id+"-trades" : "trades"
if (type == "xml") {
response_type = "application/xml"
output = convertToXml(prepare_export(batch_id))
}
else {
response_type = "text/csv"
output = convertToCsv(prepare_export(batch_id))
}
console.log(output)
downloadFile(response_type,type, filename, output)
}
function display_image(imageUrl) {
// Attempt to load the image
var img = new Image();
img.src = imageUrl;
img.onload = function() {
// If the image loads successfully, display it
$('#previewImg').attr('src', imageUrl);
//$('#imagePreview').show();
window.$('#imageModal').modal('show');
};
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
};
}
function display_batch_report(batch_id) {
//var imageUrl = '/media/report_'+data.id+".png"; // Replace with your logic to get image URL
var imageUrl = '/media/basic/'+batch_id+'.png'; // Replace with your logic to get image URL
//console.log(imageUrl)
display_image(imageUrl)
}
function refresh_logfile() {
$.ajax({
url:"/log?lines=30",
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"GET",
contentType: "application/json",
dataType: "json",
success:function(response){
if (response.lines.length == 0) {
$('#log-content').html("no records");
}
else {
$('#log-content').html(response.lines.join('\n'));
}
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
}
})
}
function delete_arch_rows(ids) {
$.ajax({
url:"/archived_runners/",
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"DELETE",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(ids),
success:function(data){
$('#delFormArchive')[0].reset();
window.$('#delModalArchive').modal('hide');
$('#deletearchive').attr('disabled', false);
//console.log(data)
archiveRecords.ajax.reload();
disable_arch_buttons()
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
$('#deletearchive').attr('disabled', false);
//archiveRecords.ajax.reload();
}
})
}
function extractNumbersFromString(str) {
// Regular expression to match the pattern #number1/number2
const pattern = /#(\d+)\/(\d+)/;
const match = str.match(pattern);
if (match) {
// Extract number1 and number2 from the match
const number1 = parseInt(match[1], 10);
const number2 = parseInt(match[2], 10);
//return { number1, number2 };
return number2;
} else {
return null;
}
}
// Function to generate a unique key for localStorage based on batch_id
function generateStorageKey(batchId) {
return 'dt-group-state-' + batchId;
}
function disable_arch_buttons() {
//disable buttons (enable on row selection)
$('#button_runagain_arch').attr('disabled','disabled');
$('#button_show_arch').attr('disabled','disabled');
$('#button_delete_arch').attr('disabled','disabled');
$('#button_delete_batch').attr('disabled','disabled');
$('#button_analyze').attr('disabled','disabled');
$('#button_edit_arch').attr('disabled','disabled');
$('#button_compare_arch').attr('disabled','disabled');
$('#button_report').attr('disabled','disabled');
$('#button_export_xml').attr('disabled','disabled');
$('#button_export_csv').attr('disabled','disabled');
}
function enable_arch_buttons() {
$('#button_analyze').attr('disabled',false);
$('#button_show_arch').attr('disabled',false);
$('#button_runagain_arch').attr('disabled',false);
$('#button_delete_arch').attr('disabled',false);
$('#button_delete_batch').attr('disabled',false);
$('#button_edit_arch').attr('disabled',false);
$('#button_compare_arch').attr('disabled',false);
$('#button_report').attr('disabled',false);
$('#button_export_xml').attr('disabled',false);
$('#button_export_csv').attr('disabled',false);
}

View File

@ -0,0 +1,478 @@
//event handlers for archiveTables
$(document).ready(function () {
initialize_archiveRecords();
archiveRecords.ajax.reload();
disable_arch_buttons();
// Use 'td:nth-child(2)' to target the second column
$('#archiveTable tbody').on('click', 'td:nth-child(2)', function () {
var data = archiveRecords.row(this).data();
//var imageUrl = '/media/report_'+data.id+".png"; // Replace with your logic to get image URL
var imageUrl = '/media/basic/'+data.id+'.png'; // Replace with your logic to get image URL
//console.log(imageUrl)
display_image(imageUrl)
});
// Use 'td:nth-child(2)' to target the second column
$('#archiveTable tbody').on('click', 'td:nth-child(18)', function () {
var data = archiveRecords.row(this).data();
if (data.batch_id) {
display_batch_report(data.batch_id)
}
});
//selectable rows in archive table
$('#archiveTable tbody').on('click', 'tr[data-group-name]', function () {
if ($(this).hasClass('selected')) {
//$(this).removeClass('selected');
//aadd here condition that disable is called only when there is no other selected class on tr[data-group-name]
// Check if there are no other selected rows before disabling buttons
if ($('#archiveTable tr[data-group-name].selected').length === 1) {
disable_arch_buttons();
}
//disable_arch_buttons()
} else {
//archiveRecords.$('tr.selected').removeClass('selected');
$(this).addClass('selected');
enable_arch_buttons()
}
});
//TOOL BUTTONs on BATCH HEADER
// Event listener for click to display batch report
$('#archiveTable tbody').on('click', 'tr.group-header #batchtool_report_button', function (event) {
event.stopPropagation();
// Get the parent <tr> element
var parentTr = $(this).closest('tr');
// Retrieve the 'data-name' attribute from the parent <tr>
var batch_id = parentTr.data('name');
display_batch_report(batch_id)
});
// Event listener for click to delete batch
$('#archiveTable tbody').on('click', 'tr.group-header #batchtool_delete_button', function (event) {
event.stopPropagation();
// Get the parent <tr> element
var parentTr = $(this).closest('tr');
// Retrieve the 'data-name' attribute from the parent <tr>
var batch_id = parentTr.data('name');
$('#batch_id_del').val(batch_id);
$('#listofids').html("");
window.$('#delModalBatch').modal('show');
});
// Event listener for click to xml export batch
$('#archiveTable tbody').on('click', 'tr.group-header #batchtool_exportxml_button', function (event) {
event.stopPropagation();
// Get the parent <tr> element
var parentTr = $(this).closest('tr');
// Retrieve the 'data-name' attribute from the parent <tr>
var batch_id = parentTr.data('name');
download_exported_data("xml", batch_id);
});
// Event listener for click to csv export batch
$('#archiveTable tbody').on('click', 'tr.group-header #batchtool_exportcsv_button', function (event) {
event.stopPropagation();
// Get the parent <tr> element
var parentTr = $(this).closest('tr');
// Retrieve the 'data-name' attribute from the parent <tr>
var batch_id = parentTr.data('name');
console.log(batch_id)
download_exported_data("csv", batch_id);
});
// Event listener for optimal batch cutoff
$('#archiveTable tbody').on('click', 'tr.group-header #batchtool_cutoff_button', function (event) {
event.stopPropagation();
// Get the parent <tr> element
var parentTr = $(this).closest('tr');
// Retrieve the 'data-name' attribute from the parent <tr>
var batch_id = parentTr.data('name');
console.log(batch_id)
analyze_optimal_cutoff(batch_id)
});
//TOOL BUTTONs above the TABLE - for selected days
//button export
$('#button_export_xml').click(function(event) {
download_exported_data("xml");
});
//button export
$('#button_export_csv').click(function(event) {
download_exported_data("csv");
});
//button select page
$('#button_selpage').click(function () {
if ($('#button_selpage').hasClass('active')) {
$('#button_selpage').removeClass('active');
archiveRecords.rows().deselect();
disable_arch_buttons();
}
else {
$('#button_selpage').addClass('active');
archiveRecords.rows( { page: 'current' } ).select();
enable_arch_buttons();
}
});
//button clear log
$('#button_clearlog').click(function () {
$('#lines').empty();
});
//button compare arch
$('#button_compare_arch').click(function () {
if (editor_diff_arch1) {editor_diff_arch1.dispose()}
if (editor_diff_stratin1) {editor_diff_stratin1.dispose()}
if (editor_diff_arch2) {editor_diff_arch2.dispose()}
if (editor_diff_stratin2) {editor_diff_stratin2.dispose()}
window.$('#diffModal').modal('show');
rows = archiveRecords.rows('.selected').data();
id1 = rows[0].id
id2 = rows[1].id
var request1 = $.ajax({
url: "/archived_runners/"+id1,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"GET",
contentType: "application/json",
dataType: "json",
success:function(data){
//console.log("first request ok")
//console.log(JSON.stringify(data,null,2));
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
console.log("first request error")
}
});
var request2 = $.ajax({
url: "/archived_runners/"+id2,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"GET",
contentType: "application/json",
dataType: "json",
success:function(data){
//console.log("first request ok")
//console.log(JSON.stringify(data,null,2));
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
console.log("first request error")
}
});
// Handling the responses of both requests
$.when(request1, request2).then(function(response1, response2) {
// Both requests have completed successfully
var result1 = response1[0];
var result2 = response2[0];
//console.log("Result from first request:", result1);
//console.log("Result from second request:", result2);
//console.log("calling compare")
perform_compare(result1, result2)
// Perform your action with the results from both requests
// Example:
}, function(error1, error2) {
// Handle errors from either request here
// Example:
console.error("Error from first request:", error1);
console.error("Error from second request:", error2);
});
//sem vstupuji dva nove natahnute objekty
function perform_compare(data1, data2) {
var record1 = new Object()
//console.log(JSON.stringify(rows))
record1 = JSON.parse(data1.strat_json)
//record1.json = rows[0].json
//record1.id = rows[0].id;
// record1.id2 = parseInt(rows[0].id2);
//record1.name = rows[0].name;
// record1.symbol = rows[0].symbol;
// record1.class_name = rows[0].class_name;
// record1.script = rows[0].script;
// record1.open_rush = rows[0].open_rush;
// record1.close_rush = rows[0].close_rush;
//console.log(record1.stratvars_conf)
//ELEMENTS TO COMPARE
//profit sekce
//console.log(data1.metrics)
try {
record1["profit"] = JSON.parse(data1.metrics.profit)
}
catch (e) {
console.log(e.message)
}
//record1.stratvars_conf = TOML.parse(record1.stratvars_conf);
//record1.add_data_conf = TOML.parse(record1.add_data_conf);
// record1.note = rows[0].note;
// record1.history = "";
//jsonString1 = JSON.stringify(record1, null, 2);
var record2 = new Object()
record2 = JSON.parse(data2.strat_json)
// record2.id = rows[1].id;
// record2.id2 = parseInt(rows[1].id2);
//record2.name = rows[1].name;
// record2.symbol = rows[1].symbol;
// record2.class_name = rows[1].class_name;
// record2.script = rows[1].script;
// record2.open_rush = rows[1].open_rush;
// record2.close_rush = rows[1].close_rush;
//ELEMENTS TO COMPARE
//console.log(data2.metrics)
try {
record2["profit"] = JSON.parse(data2.metrics.profit)
}
catch (e) {
console.log(e.message)
}
//record2.stratvars_conf = TOML.parse(record2.stratvars_conf);
//record2.add_data_conf = TOML.parse(record2.add_data_conf);
// record2.note = rows[1].note;
// record2.history = "";
//jsonString2 = JSON.stringify(record2, null, 2);
$('#diff_first').text(record1.name);
$('#diff_second').text(record2.name);
$('#diff_first_id').text(data1.id);
$('#diff_second_id').text(data2.id);
//monaco
require(["vs/editor/editor.main"], () => {
editor_diff_arch1 = monaco.editor.createDiffEditor(document.getElementById('diff_content1'),
{
language: 'toml',
theme: 'tomlTheme-dark',
originalEditable: false,
automaticLayout: true
}
);
console.log(record1.stratvars_conf)
console.log(record2.stratvars_conf)
editor_diff_arch1.setModel({
original: monaco.editor.createModel(record1.stratvars_conf, 'toml'),
modified: monaco.editor.createModel(record2.stratvars_conf, 'toml'),
});
editor_diff_arch2 = monaco.editor.createDiffEditor(document.getElementById('diff_content2'),
{
language: 'toml',
theme: 'tomlTheme-dark',
originalEditable: false,
automaticLayout: true
}
);
editor_diff_arch2.setModel({
original: monaco.editor.createModel(record1.add_data_conf, 'toml'),
modified: monaco.editor.createModel(record2.add_data_conf, 'toml'),
});
});
// var delta = compareObjects(record1, record2)
// const htmlMarkup2 = `<pre>{\n${generateHTML(record2, delta)}}\n</pre>`;
// document.getElementById('second').innerHTML = htmlMarkup2;
// const htmlMarkup1 = `<pre>{\n${generateHTML(record1, delta)}}\n</pre>`;
// document.getElementById('first').innerHTML = htmlMarkup1;
event.preventDefault();
//$('#button_compare').attr('disabled','disabled');
}
});
//generate batch optimization cutoff (predelat na button pro obecne analyzy batche)
$('#button_analyze').click(function () {
analyze_optimal_cutoff();
});
//generate report button
$('#button_report').click(function () {
rows = archiveRecords.rows('.selected');
if (rows == undefined) {
return
}
$('#button_report').attr('disabled','disabled');
runnerIds = []
if(rows.data().length > 0 ) {
// Loop through the selected rows and display an alert with each row's ID
rows.every(function (rowIdx, tableLoop, rowLoop ) {
var data = this.data()
runnerIds.push(data.id);
});
}
$.ajax({
url:"/archived_runners/generatereportimage",
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"POST",
xhrFields: {
responseType: 'blob'
},
contentType: "application/json",
processData: false,
data: JSON.stringify(runnerIds),
success:function(blob){
var url = window.URL || window.webkitURL;
console.log("vraceny obraz", blob)
console.log("url",url.createObjectURL(blob))
display_image(url.createObjectURL(blob))
$('#button_report').attr('disabled',false);
},
error: function(xhr, status, error) {
console.log("proc to skace do erroru?")
//window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
$('#button_report').attr('disabled',false);
}
})
});
//button to query log
$('#logRefreshButton').click(function () {
refresh_logfile()
});
//button to open log modal
$('#button_show_log').click(function () {
window.$('#logModal').modal('show');
refresh_logfile()
});
//delete batch button - open modal - DECOMISS - dostupne jen na batche
// $('#button_delete_batch').click(function () {
// row = archiveRecords.row('.selected').data();
// if (row == undefined || row.batch_id == undefined) {
// return
// }
// $('#batch_id_del').val(row.batch_id);
// rows = archiveRecords.rows('.selected');
// if (rows == undefined) {
// return
// }
// $('#listofids').html("");
// window.$('#delModalBatch').modal('show');
// });
//delete batch submit modal
$("#delModalBatch").on('submit','#delFormBatch', delete_batch);
//delete arch button - open modal
$('#button_delete_arch').click(function () {
rows = archiveRecords.rows('.selected');
if (rows == undefined) {
return
}
$('#listofids').html("");
if(rows.data().length > 0 ) {
ids_to_del = ""
// Loop through the selected rows and display an alert with each row's ID
rows.every(function (rowIdx, tableLoop, rowLoop ) {
var data = this.data()
ids_to_del = ids_to_del + data.id + "<br>"
});
$('#listofids').html(ids_to_del);
window.$('#delModalArchive').modal('show');
//$('#delidarchive').val(row.id);
}
});
//edit button
$('#button_edit_arch').click(function () {
row = archiveRecords.row('.selected').data();
if (row == undefined) {
return
}
refresh_arch_and_callback(row, display_edit_modal)
function display_edit_modal(row) {
window.$('#editModalArchive').modal('show');
$('#editidarchive').val(row.id);
$('#editnote').val(row.note);
try {
metrics = JSON.parse(row.metrics)
}
catch (e) {
metrics = row.metrics
}
$('#metrics').val(JSON.stringify(metrics,null,2));
//$('#metrics').val(TOML.parse(row.metrics));
if (row.stratvars_toml) {
$('#editstratvars').val(row.stratvars_toml);
}
else{
$('#editstratvars').val(JSON.stringify(row.stratvars,null,2));
}
$('#editstratjson').val(row.strat_json);
}
});
//show button
$('#button_show_arch').click(function () {
row = archiveRecords.row('.selected').data();
if (row == undefined) {
return
}
refresh_arch_and_callback(row, get_detail_and_chart)
});
//run again button
$('#button_runagain_arch').click(run_day_again)
//workaround pro spatne oznacovani selectu i pro group-headery
// $('#archiveTable tbody').on('click', 'tr.group-header', function(event) {
// var $row = $(this);
// // Schedule the class removal/addition for the next event loop
// setTimeout(function() {
// if ($row.hasClass("selected")) {
// console.log("Header selected, removing selection");
// $row.removeClass("selected");
// }
// }, 0);
// });
// Expand/Collapse functionality
$('#archiveTable tbody').on('click', 'tr.group-header', expand_collapse_rows);
})

View File

@ -0,0 +1,412 @@
var archiveRecords = null
//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: [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',
})
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>'
}
},
},
{
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',
})
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)
//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 {
profit = existingBatch.profit
itemCount = existingBatch.itemCount
period = existingBatch.period
started = existingBatch.started
stratinId = existingBatch.stratinId
}
}
//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>'
//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)
profit_icon_color = (profit>0) ? "#4f8966" : "#bb2f5e" //"#d42962"
}
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>'
//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);
}
},
drawCallback: function (settings) {
//console.log("drawcallback", configData)
setTimeout(function(){
//populate all tool buttons on batch header
// Loop over all divs with the class 'batch-buttons-container'
if (configData["dynamic_buttons"]) {
//console.log("jsme tu po cekani")
//console.log("pred loopem")
$('.batch_buttons_container').each((index, element) => {
//console.log("jsme uvnitr foreach");
idecko = $(element).attr('id')
//console.log("idecko", idecko)
var batchId = $(element).data('batch-id'); // Get the data-batch-id attribute
//console.log("nalezeno pred", batchId, $(element));
populate_dynamic_buttons($(element), configData["dynamic_buttons"], batchId);
//console.log("po", $(element));
});
}else {
console.log("no dynamic_buttons configuration loaded")
}
}, 1);
// var api = this.api();
// var rows = api.rows({ page: 'current' }).nodes();
// api.column(17, { page: 'current' }).data().each(function (group, i) {
// console.log("drawCallabck i",i)
// console.log("rows", $(rows).eq(i))
// var groupName = group ? group : $(rows).eq(i).attr('data-name');
// console.log("groupName", groupName)
// var stateKey = 'dt-group-state-' + groupName;
// var state = localStorage.getItem(stateKey);
// if (state === 'collapsed') {
// $(rows).eq(i).hide();
// } else {
// $(rows).eq(i).show();
// }
// Set the unique identifier as a data attribute on each row
//$(rows).eq(i).attr('data-group-name', groupName);
// // Add or remove the 'collapsed' class based on the state
// if (groupName.startsWith('no-batch-id-')) {
// $('tr[data-name="' + groupName + '"]').toggleClass('collapsed', state === 'collapsed');
// }
// });
}
});
}

View File

@ -0,0 +1,55 @@
//mozna dat do dokument ready a rozdelit na handlers a funkce
//edit modal
$("#editModalArchive").on('submit','#editFormArchive', function(event){
event.preventDefault();
$('#editarchive').attr('disabled','disabled');
trow = archiveRecords.row('.selected').data();
note = $('#editnote').val()
var formData = $(this).serializeJSON();
row = {}
row["id"] = trow.id
row["note"] = note
jsonString = JSON.stringify(row);
//console.log("pred odeslanim json string", jsonString)
$.ajax({
url:"/archived_runners/"+trow.id,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-API-Key',
API_KEY); },
method:"PATCH",
contentType: "application/json",
// dataType: "json",
data: jsonString,
success:function(data){
$('#editFormArchive')[0].reset();
window.$('#editModalArchive').modal('hide');
$('#editarchive').attr('disabled', false);
archiveRecords.ajax.reload();
disable_arch_buttons();
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
window.alert(JSON.stringify(xhr));
console.log(JSON.stringify(xhr));
$('#editarchive').attr('disabled', false);
}
})
});
//delete modal
$("#delModalArchive").on('submit','#delFormArchive', function(event){
event.preventDefault();
$('#deletearchive').attr('disabled','disabled');
//rows = archiveRecords.rows('.selected');
if(rows.data().length > 0 ) {
runnerIds = []
// Loop through the selected rows and display an alert with each row's ID
rows.every(function (rowIdx, tableLoop, rowLoop ) {
var data = this.data()
runnerIds.push(data.id);
});
delete_arch_rows(runnerIds)
}
});

View File

View File

@ -0,0 +1,41 @@
// Function to convert a JavaScript object to CSV
function convertToCsv(data) {
var csv = '';
// Get the headers
var headers = Object.keys(data[0]);
csv += headers.join(',') + '\n';
// Iterate over the data
data.forEach(function (item) {
var row = headers.map(function (header) {
return item[header];
});
csv += row.join(',') + '\n';
});
return csv;
}
//type ("text/csv","application/xml"), filetype (csv), filename
function downloadFile(type, filetype, filename, content) {
var blob = new Blob([content], { type: type });
var url = window.URL.createObjectURL(blob);
var link = document.createElement("a");
link.href = url;
link.download = filename +"."+filetype;
link.click();
}
// Function to convert a JavaScript object to XML
function convertToXml(data) {
var xml = '<?xml version="1.0" encoding="UTF-8"?>\n<trades>\n';
data.forEach(function (item) {
xml += ' <trade>\n';
Object.keys(item).forEach(function (key) {
xml += ' <' + key + '>' + item[key] + '</' + key + '>\n';
});
xml += ' </trade>\n';
});
xml += '</trades>';
return xml;
}

View File

@ -1,3 +1,5 @@
//general util functions all accross
API_KEY = localStorage.getItem("api-key")
var chart = null

View File

@ -20,13 +20,24 @@
overflow: visible;
}
.expand-icon {
margin-right: 3px;
vertical-align: middle;
font-size: 15px;
color: var(--bs-gray-400);
border-radius: 4px;
/* padding: 2px; */
}
.tool-icon {
margin-right: 0px;
vertical-align: middle;
font-size: 19px;
color: var(--bs-secondary);
border-radius: 4px;
padding: 2px;
/* padding: 2px; */
padding-left: 2px;
padding-right: 2px;
}
.tool-icon:hover {
@ -34,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;
@ -254,6 +328,15 @@ table.dataTable thead>tr>th.sorting_asc:before, table.dataTable thead>tr>th.sort
margin-right: 5px;
vertical-align: middle;
}
/* Base style for color-tag */
/* Additional class to change the triangle direction when the row is expanded */
.collapsed .color-tag {
border-top: none;
border-bottom: 10px solid #838E65; /* Triangle pointing down when expanded */
}
.input-group { margin-top: 10px; display: none; }
.group-header {
cursor: pointer;
@ -262,23 +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 */
.batchtool {
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 immediately when hovering over a .group-header row */
.group-header:hover .batchtool {
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: bold; */
font-weight: 400;
vertical-align: super;
}
.group-header .batchheader-period-info {
color: #a1a1a1; /* Highlight period info */
vertical-align: super;
/* font-weight: bold; */
}
@ -342,7 +449,7 @@ html {
}
.flex-container {
display: inline-grid;
/* display: inline-grid; */
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
@ -409,6 +516,14 @@ html {
width: 35%;
}
}
/*
.even {
display: block;
}
.odd {
display: block;
} */
@media (min-width: 2001px) {
.msgContainerInner {