From 67d34481c6cfd6df4d7e2d6bc8e1e40cb58e7d89 Mon Sep 17 00:00:00 2001 From: David Brazda Date: Mon, 20 Nov 2023 17:55:55 +0100 Subject: [PATCH] sizing, progressionbar,reporting basics --- media/basic/.gitignore | 5 + v2realbot/common/PrescribedTradeModel.py | 2 + v2realbot/config.py | 4 + v2realbot/controller/services.py | 143 +++++-- v2realbot/loader/trade_offline_streamer.py | 6 +- v2realbot/main.py | 34 +- v2realbot/reporting/__init__.py | 0 v2realbot/reporting/metricstools.py | 145 +++++++ v2realbot/reporting/metricstoolsimage.py | 382 ++++++++++++++++++ v2realbot/static/index.html | 5 +- v2realbot/static/js/archivetables.js | 92 +++++ v2realbot/static/main.css | 9 +- v2realbot/strategy/StrategyClassicSL.py | 48 ++- v2realbot/strategy/base.py | 1 + .../strategyblocks/indicators/helpers.py | 5 +- v2realbot/strategyblocks/newtrade/signals.py | 9 +- v2realbot/strategyblocks/newtrade/sizing.py | 143 +++++++ v2realbot/tools/sizingpattervisual.py | 29 ++ v2realbot/utils/utils.py | 24 ++ 19 files changed, 1032 insertions(+), 54 deletions(-) create mode 100644 media/basic/.gitignore create mode 100644 v2realbot/reporting/__init__.py create mode 100644 v2realbot/reporting/metricstools.py create mode 100644 v2realbot/reporting/metricstoolsimage.py create mode 100644 v2realbot/strategyblocks/newtrade/sizing.py create mode 100644 v2realbot/tools/sizingpattervisual.py diff --git a/media/basic/.gitignore b/media/basic/.gitignore new file mode 100644 index 0000000..0baf103 --- /dev/null +++ b/media/basic/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* + +# Except this file +!.gitignore \ No newline at end of file diff --git a/v2realbot/common/PrescribedTradeModel.py b/v2realbot/common/PrescribedTradeModel.py index 6bfa6a2..db14bff 100644 --- a/v2realbot/common/PrescribedTradeModel.py +++ b/v2realbot/common/PrescribedTradeModel.py @@ -30,6 +30,8 @@ class Trade(BaseModel): entry_price: Optional[float] = None goal_price: Optional[float] = None size: Optional[int] = None + # size_multiplier je pomocna promenna pro pocitani relativniho denniho profit + size_multiplier: Optional[float] = None # stoploss_type: TradeStoplossType stoploss_value: Optional[float] = None profit: Optional[float] = 0 diff --git a/v2realbot/config.py b/v2realbot/config.py index 452ec5c..ea8f408 100644 --- a/v2realbot/config.py +++ b/v2realbot/config.py @@ -1,6 +1,10 @@ from alpaca.data.enums import DataFeed from v2realbot.enums.enums import Mode, Account, FillCondition from appdirs import user_data_dir +from pathlib import Path + +#directory for generated images and basic reports +MEDIA_DIRECTORY = Path(__file__).parent.parent / "media" #'0.0.0.0', #currently only prod server has acces to LIVE diff --git a/v2realbot/controller/services.py b/v2realbot/controller/services.py index 33a8673..8665d88 100644 --- a/v2realbot/controller/services.py +++ b/v2realbot/controller/services.py @@ -13,7 +13,7 @@ from v2realbot.utils.ilog import delete_logs from v2realbot.common.PrescribedTradeModel import Trade, TradeDirection, TradeStatus, TradeStoplossType from datetime import datetime from threading import Thread, current_thread, Event, enumerate -from v2realbot.config import STRATVARS_UNCHANGEABLES, ACCOUNT1_PAPER_API_KEY, ACCOUNT1_PAPER_SECRET_KEY, ACCOUNT1_LIVE_API_KEY, ACCOUNT1_LIVE_SECRET_KEY, DATA_DIR,BT_FILL_CONS_TRADES_REQUIRED,BT_FILL_LOG_SURROUNDING_TRADES,BT_FILL_CONDITION_BUY_LIMIT,BT_FILL_CONDITION_SELL_LIMIT, GROUP_TRADES_WITH_TIMESTAMP_LESS_THAN +from v2realbot.config import STRATVARS_UNCHANGEABLES, ACCOUNT1_PAPER_API_KEY, ACCOUNT1_PAPER_SECRET_KEY, ACCOUNT1_LIVE_API_KEY, ACCOUNT1_LIVE_SECRET_KEY, DATA_DIR,BT_FILL_CONS_TRADES_REQUIRED,BT_FILL_LOG_SURROUNDING_TRADES,BT_FILL_CONDITION_BUY_LIMIT,BT_FILL_CONDITION_SELL_LIMIT, GROUP_TRADES_WITH_TIMESTAMP_LESS_THAN, MEDIA_DIRECTORY import importlib from alpaca.trading.requests import GetCalendarRequest from alpaca.trading.client import TradingClient @@ -35,6 +35,8 @@ import v2realbot.strategyblocks.indicators.custom as ci from v2realbot.strategyblocks.inits.init_indicators import initialize_dynamic_indicators from v2realbot.strategyblocks.indicators.indicators_hub import populate_dynamic_indicators from v2realbot.interfaces.backtest_interface import BacktestInterface +import os +from v2realbot.reporting.metricstoolsimage import generate_trading_report_image #from pyinstrument import Profiler #adding lock to ensure thread safety of TinyDB (in future will be migrated to proper db) @@ -332,6 +334,12 @@ def capsule(target: object, db: object, inter_batch_params: dict = None): archive_runner(runner=i, strat=target, inter_batch_params=inter_batch_params) #mazeme runner po skonceni instance db.runners.remove(i) + #vytvoreni report image pro RUNNER + try: + generate_trading_report_image(runner_ids=[str(i.id)]) + print("DAILY REPORT IMAGE CREATED") + except Exception as e: + print("Nepodarilo se vytvorit report image", str(e)+format_exc()) print("Runner STOPPED") @@ -500,6 +508,13 @@ def batch_run_manager(id: UUID, runReq: RunRequest, rundays: list[RunDay]): #i.history += str(runner.__dict__)+"
" db.save() + #vytvoreni report image pro batch + try: + generate_trading_report_image(batch_id=batch_id) + print("BATCH REPORT IMAGE CREATED") + except Exception as e: + print("Nepodarilo se vytvorit report image", str(e)+format_exc()) + #stratin run def run_stratin(id: UUID, runReq: RunRequest, synchronous: bool = False, inter_batch_params: dict = None): if runReq.mode == Mode.BT: @@ -676,12 +691,6 @@ def populate_metrics_output_directory(strat: StrategyInstance, inter_batch_param res["profit"]["batch_sum_profit"] = int(inter_batch_params["batch_profit"]) res["profit"]["batch_sum_rel_profit"] = inter_batch_params["batch_rel_profit"] - #rel_profit zprumerovane - res["profit"]["daily_rel_profit_avg"] = float(np.sum(strat.state.rel_profit_cum)) if len(strat.state.rel_profit_cum) > 0 else 0 - #rel_profit rozepsane zisky - res["profit"]["daily_rel_profit_list"] = strat.state.rel_profit_cum - - #metrikz z prescribedTrades, pokud existuji try: long_profit = 0 @@ -696,39 +705,53 @@ def populate_metrics_output_directory(strat: StrategyInstance, inter_batch_param max_loss_time = None long_cnt = 0 short_cnt = 0 + sum_wins_profit= 0 + sum_loss = 0 if "prescribedTrades" in strat.state.vars: for trade in strat.state.vars.prescribedTrades: - if trade.profit_sum < max_loss: - max_loss = trade.profit_sum - max_loss_time = trade.last_update - if trade.profit_sum > max_profit: - max_profit = trade.profit_sum - max_profit_time = trade.last_update - if trade.status == TradeStatus.ACTIVATED and trade.direction == TradeDirection.LONG: - long_cnt += 1 - if trade.profit is not None: - long_profit += trade.profit - if trade.profit < 0: - long_losses += trade.profit - if trade.profit > 0: - long_wins += trade.profit - if trade.status == TradeStatus.ACTIVATED and trade.direction == TradeDirection.SHORT: - short_cnt +=1 - if trade.profit is not None: - short_profit += trade.profit - if trade.profit < 0: - short_losses += trade.profit - if trade.profit > 0: - short_wins += trade.profit + if trade.status == TradeStatus.CLOSED: + if trade.profit_sum < max_loss: + max_loss = trade.profit_sum + max_loss_time = trade.last_update + if trade.profit_sum > max_profit: + max_profit = trade.profit_sum + max_profit_time = trade.last_update + if trade.direction == TradeDirection.LONG: + long_cnt += 1 + if trade.profit is not None: + long_profit += trade.profit + if trade.profit < 0: + long_losses += trade.profit + if trade.profit > 0: + long_wins += trade.profit + if trade.direction == TradeDirection.SHORT: + short_cnt +=1 + if trade.profit is not None: + short_profit += trade.profit + if trade.profit < 0: + short_losses += trade.profit + if trade.profit > 0: + short_wins += trade.profit + sum_wins = long_wins + short_wins + sum_losses = long_losses + short_losses + #toto nejak narovnat, mozna diskutovat s Martinem nebo s Vercou + + #zatim to neukazuje moc jasne - poznámka: ztráta by měla být jenom negativní profit, nikoliv nová veličina + #jediná vyjímka je u max.kumulativní ztráty (drawdown) + res["profit"]["sum_wins"] = sum_wins + res["profit"]["sum_losses"] = sum_losses res["profit"]["long_cnt"] = long_cnt - res["profit"]["short_cnt"] = short_cnt + res["profit"]["short_cnt"] = short_cnt + #celkovy profit za long/short res["profit"]["long_profit"] = round(long_profit,2) res["profit"]["short_profit"] = round(short_profit,2) - res["profit"]["max_profit"] = round(max_profit,2) - res["profit"]["max_profit_time"] = str(max_profit_time) - res["profit"]["max_loss"] = round(max_loss,2) - res["profit"]["max_loss_time"] = str(max_loss_time) + #maximalni kumulativni profit (tzn. peaky profitu) + res["profit"]["max_profit_cum"] = round(max_profit,2) + res["profit"]["max_profit_cum_time"] = str(max_profit_time) + #maximalni kumulativni ztrata (tzn. peaky v lossu) + res["profit"]["max_loss_cum"] = round(max_loss,2) + res["profit"]["max_loss_time_cum"] = str(max_loss_time) res["profit"]["long_wins"] = round(long_wins,2) res["profit"]["long_losses"] = round(long_losses,2) res["profit"]["short_wins"] = round(short_wins,2) @@ -739,7 +762,13 @@ def populate_metrics_output_directory(strat: StrategyInstance, inter_batch_param rp_string = "RP" + str(float(np.sum(strat.state.rel_profit_cum))) if len(strat.state.rel_profit_cum) >0 else "noRP" ##summary pro rychle zobrazeni P333L-222 PT9:30 PL10:30 - res["profit"]["sum"]="P"+str(int(max_profit))+"L"+str(int(max_loss))+" "+ mpt_string+" " + mlt_string + rp_string + " "+str(strat.state.rel_profit_cum) + res["profit"]["sum"]="P"+str(int(sum_wins))+"L"+str(int(sum_losses))+" "+"MCP"+str(int(max_profit))+"MCL(DD)"+str(int(max_loss))+" "+ mpt_string+" " + mlt_string + rp_string + " "+str(strat.state.rel_profit_cum) + + #rel_profit zprumerovane + res["profit"]["daily_rel_profit_sum"] = float(np.sum(strat.state.rel_profit_cum)) if len(strat.state.rel_profit_cum) > 0 else 0 + #rel_profit rozepsane zisky + res["profit"]["daily_rel_profit_list"] = strat.state.rel_profit_cum + #vlozeni celeho listu res["prescr_trades"]=json.loads(json.dumps(strat.state.vars.prescribedTrades, default=json_serial)) @@ -1007,6 +1036,25 @@ def edit_archived_runners(runner_id: UUID, archChange: RunArchiveChange): print(errmsg) return -2, errmsg + +def delete_report_files(id): + + #ZATIM MAME JEN BASIC + #delete report images + image_file_name = f"{id}.png" + image_path = str(MEDIA_DIRECTORY / "basic" / image_file_name) + try: + if os.path.exists(image_path): + os.remove(image_path) + print(f"File {image_path} has been deleted.") + return (0, "deleted") + else: + print(f"No File {image_path} found to delte.") + return (1, "not found") + except Exception as e: + print(f"An error occurred while deleting the file: {e}") + return (-1, str(e)) + #delete runner in archive and archive detail and runner logs #predelano do JEDNE TRANSAKCE def delete_archived_runners_byIDs(ids: list[UUID]): @@ -1016,6 +1064,19 @@ def delete_archived_runners_byIDs(ids: list[UUID]): for id in ids: c = conn.cursor() print(str(id)) + + # Get batch_id for the current runner_id + c.execute("SELECT batch_id FROM runner_header WHERE runner_id = ?", (str(id),)) + batch_id = c.fetchone() + if batch_id: + batch_id = batch_id[0] + # Check if this is the last record with the given batch_id + c.execute("SELECT COUNT(*) FROM runner_header WHERE batch_id = ?", (batch_id,)) + count = c.fetchone()[0] + if count == 1: + # If it's the last record, call delete_report_files + delete_report_files(batch_id) + resh = c.execute(f"DELETE from runner_header WHERE runner_id='{str(id)}';") print("header deleted",resh.rowcount) resd = c.execute(f"DELETE from runner_detail WHERE runner_id='{str(id)}';") @@ -1025,6 +1086,9 @@ def delete_archived_runners_byIDs(ids: list[UUID]): out.append(str(id) + ": " + str(resh.rowcount) + " " + str(resd.rowcount) + " " + str(resl.rowcount)) conn.commit() print("commit") + + delete_report_files(id) + # if resh.rowcount == 0 or resd.rowcount == 0: # return -1, "not found "+str(resh.rowcount) + " " + str(resd.rowcount) + " " + str(resl.rowcount) return 0, out @@ -1044,6 +1108,15 @@ def delete_archive_header_byID(id: UUID): res = execute_with_retry(c,statement) conn.commit() print("deleted", res.rowcount) + #delete report images + image_file_name = f"report_{id}.png" + image_path = str(MEDIA_DIRECTORY / image_file_name) + try: + if os.path.exists(image_path): + os.remove(image_path) + print(f"File {image_path} has been deleted.") + except Exception as e: + print(f"An error occurred while deleting the file: {e}") finally: pool.release_connection(conn) return res.rowcount diff --git a/v2realbot/loader/trade_offline_streamer.py b/v2realbot/loader/trade_offline_streamer.py index 44840d7..ad68051 100644 --- a/v2realbot/loader/trade_offline_streamer.py +++ b/v2realbot/loader/trade_offline_streamer.py @@ -8,7 +8,7 @@ from alpaca.data.enums import DataFeed from alpaca.data.historical import StockHistoricalDataClient from alpaca.data.requests import StockLatestQuoteRequest, StockBarsRequest, StockTradesRequest from threading import Thread, current_thread -from v2realbot.utils.utils import parse_alpaca_timestamp, ltp, zoneNY, print +from v2realbot.utils.utils import parse_alpaca_timestamp, ltp, zoneNY from v2realbot.utils.tlog import tlog from datetime import datetime, timedelta, date from threading import Thread @@ -21,6 +21,8 @@ import os from rich import print import queue from alpaca.trading.models import Calendar +from tqdm import tqdm + """ Trade offline data streamer, based on Alpaca historical data. """ @@ -212,7 +214,7 @@ class Trade_Offline_Streamer(Thread): cnt = 1 - for t in tradesResponse[symbol]: + for t in tqdm(tradesResponse[symbol]): #protoze je zde cely den, poustime dal, jen ty relevantni #pokud je start_time < trade < end_time diff --git a/v2realbot/main.py b/v2realbot/main.py index 2407c7c..c9fed75 100644 --- a/v2realbot/main.py +++ b/v2realbot/main.py @@ -1,6 +1,6 @@ import os,sys sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from v2realbot.config import WEB_API_KEY, DATA_DIR +from v2realbot.config import WEB_API_KEY, DATA_DIR, MEDIA_DIRECTORY from alpaca.data.timeframe import TimeFrame, TimeFrameUnit from datetime import datetime import os @@ -13,7 +13,7 @@ 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, RunArchiveDetail, Bar, RunArchiveChange, TestList, ConfigItem, InstantIndicator from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException, status, WebSocketException, Cookie, Query -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from fastapi.security import HTTPBasic, HTTPBasicCredentials from v2realbot.enums.enums import Env, Mode @@ -30,6 +30,9 @@ from v2realbot.utils.sysutils import get_environment from uuid import uuid4 from sqlite3 import OperationalError 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 async io import Queue, QueueEmpty # install() @@ -64,6 +67,7 @@ def api_key_auth(api_key: str = Depends(X_API_KEY)): app = FastAPI() root = os.path.dirname(os.path.abspath(__file__)) app.mount("/static", StaticFiles(html=True, directory=os.path.join(root, 'static')), name="static") +app.mount("/media", StaticFiles(directory=str(MEDIA_DIRECTORY)), name="media") #app.mount("/", StaticFiles(html=True, directory=os.path.join(root, 'static')), name="www") security = HTTPBasic() @@ -459,7 +463,6 @@ def _delete_indicator_byName(runner_id: UUID, indicator: InstantIndicator): raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not changed: {res}:{runner_id}:{vals}") - #edit archived runner ("note",..) @app.patch("/archived_runners/{runner_id}", dependencies=[Depends(api_key_auth)]) def _edit_archived_runners(archChange: RunArchiveChange, runner_id: UUID): @@ -509,6 +512,31 @@ def _get_alpaca_history_bars(symbol: str, datetime_object_from: datetime, dateti else: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found {res} {set}") +#get pdf report - WIP +@app.put("/archived_runners/{runner_id}/generatepdf", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"application/pdf": {}}}}) +def _generat_pdf(runner_id: UUID): + #jako vstup umouznit i seznam runneru - vytvori to pote report ze vsech techto + #pripadne mit jako vstup batch a udelat to pro batch () + res, vals = mt.create_trading_report_pdf(id=runner_id) + if res == 0: + # Return the PDF data as a streaming response {str(runner_id)} + return StreamingResponse(vals, media_type="application/pdf", headers={"Content-Disposition": "attachment; filename=report.pdf"}) + elif res == -1: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error no runner: {runner_id} {res}:{vals}") + else: + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not changed: {res}:{runner_id}:{vals}") + +#generate image based list of ids +@app.post("/archived_runners/generatereportimage", dependencies=[Depends(api_key_auth)], responses={200: {"content": {"image/png": {}}}}) +def _generate_report_image(runner_ids: list[UUID]): + try: + res, stream = generate_trading_report_image(runner_ids=runner_ids,stream=True) + if res == 0: return StreamingResponse(stream, media_type="image/png",headers={"Content-Disposition": "attachment; filename=report.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)]) def create_record(testlist: TestList): diff --git a/v2realbot/reporting/__init__.py b/v2realbot/reporting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v2realbot/reporting/metricstools.py b/v2realbot/reporting/metricstools.py new file mode 100644 index 0000000..1517428 --- /dev/null +++ b/v2realbot/reporting/metricstools.py @@ -0,0 +1,145 @@ +import json +import numpy as np +import matplotlib +matplotlib.use('Agg') # Set the Matplotlib backend to 'Agg' +import matplotlib.pyplot as plt +import seaborn as sns +import pandas as pd +from fpdf import FPDF, XPos, YPos +from datetime import datetime +from io import BytesIO +import v2realbot.controller.services as cs +from rich import print +def create_trading_report_pdf(id, direct = True, output_file='trading_report.pdf'): + + #get runner + res, set =cs.get_archived_runner_header_byID(id) + if res != 0: + return -1, f"no runner {id} found" + + print("archrunner") + print(set) + + # Parse JSON data + data = set.metrics + profit_data = data["profit"] + pos_cnt_data = data["pos_cnt"] + prescr_trades_data = data["prescr_trades"] + + # PDF setup + pdf = FPDF() + pdf.set_auto_page_break(auto=True, margin=15) + pdf.set_font("Helvetica", size=10) + + # Start the first page for plots + pdf.add_page() + + # Create a combined figure for all plots (adjusting the layout to 3x3) + fig, axs = plt.subplots(3, 3, figsize=(15, 15)) + + # Plot 1: Overall Profit Summary Chart + sns.barplot(x=['Total Wins', 'Total Losses', 'Net Profit'], + y=[profit_data["sum_wins"], profit_data["sum_losses"], + profit_data["sum_wins"] - profit_data["sum_losses"]], + ax=axs[0, 0]) + axs[0, 0].set_title('Overall Profit Summary') + + # Plot 2: Profit Distribution by Trade Type + axs[0, 1].pie([profit_data["long_profit"], profit_data["short_profit"]], + labels=['Long Profit', 'Short Profit'], autopct='%1.1f%%') + axs[0, 1].set_title('Profit Distribution by Trade Type') + + # Plot 3: Cumulative Profit Over Time Line Chart + exit_times = [datetime.fromtimestamp(trade["exit_time"]) for trade in prescr_trades_data] + cumulative_profits = [trade["profit_sum"] for trade in prescr_trades_data] + sns.lineplot(x=exit_times, y=cumulative_profits, ax=axs[0, 2]) + axs[0, 2].set_title('Cumulative Profit Over Time') + axs[0, 2].tick_params(axis='x', rotation=45) + + # Plot 4: Cumulative Profit Over Time with Max Profit Point + sns.lineplot(x=exit_times, y=cumulative_profits, label='Cumulative Profit', ax=axs[1, 0]) + max_profit_time = datetime.fromisoformat(profit_data["max_profit_cum_time"]) + max_profit = profit_data["max_profit_cum"] + axs[1, 0].scatter(max_profit_time, max_profit, color='green', label='Max Profit') + axs[1, 0].set_title('Cumulative Profit Over Time with Max Profit Point') + axs[1, 0].tick_params(axis='x', rotation=45) + axs[1, 0].legend() + + # Plot 5: Trade Counts Bar Chart + sns.barplot(x=['Long Trades', 'Short Trades'], + y=[profit_data["long_cnt"], profit_data["short_cnt"]], + ax=axs[1, 1]) + axs[1, 1].set_title('Trade Counts') + + # Plot 6: Position Size Distribution + sns.barplot(x=list(pos_cnt_data.keys()), y=list(pos_cnt_data.values()), ax=axs[1, 2]) + axs[1, 2].set_title('Position Size Distribution') + + # Plot 7: Daily Relative Profit Chart + sns.lineplot(x=range(len(profit_data["daily_rel_profit_list"])), y=profit_data["daily_rel_profit_list"], ax=axs[2, 0]) + axs[2, 0].set_title('Daily Relative Profit') + axs[2, 0].set_xlabel('Trade Number') + axs[2, 0].set_ylabel('Relative Profit') + + # Adjust layout, save the combined plot, and add it to the PDF + # plt.tight_layout() + # plt.savefig("combined_plot.png", format="png", bbox_inches="tight") + # plt.close() + # pdf.image("combined_plot.png", x=10, y=20, w=180) + + plt.tight_layout() + plot_buffer = BytesIO() + plt.savefig(plot_buffer, format="png") + plt.close() + plot_buffer.seek(0) + pdf.image(plot_buffer, x=10, y=20, w=180) + plot_buffer.close() + + # Start a new page for the table and additional information + pdf.add_page() + + # 8. Individual Trade Details Table + pdf.set_font("Helvetica", size=8) + trade_fields = ['id', 'direction', 'entry_time', 'exit_time', 'profit', 'profit_sum', 'rel_profit'] + trades_table_data = [{field: trade[field] for field in trade_fields} for trade in prescr_trades_data] + trades_table = pd.DataFrame(trades_table_data) + for row in trades_table.values: + for cell in row: + pdf.cell(40, 10, str(cell), border=1) + pdf.ln() + + # Profit/Loss Ratio and Relative Profit Metrics + profit_loss_ratio = "N/A" if profit_data["sum_losses"] == 0 else str(profit_data["sum_wins"] / profit_data["sum_losses"]) + relative_profit = profit_data["daily_rel_profit_sum"] + pdf.cell(0, 10, f"Profit/Loss Ratio: {profit_loss_ratio}", new_x=XPos.LMARGIN, new_y=YPos.NEXT) + pdf.cell(0, 10, f"Total Relative Profit: {relative_profit}", new_x=XPos.LMARGIN, new_y=YPos.NEXT) + + # Summary of Key Metrics + pdf.cell(0, 10, "\nSummary of Key Metrics:", new_x=XPos.LMARGIN, new_y=YPos.NEXT) + pdf.cell(0, 10, f"Total Number of Trades: {profit_data['long_cnt'] + profit_data['short_cnt']}", new_x=XPos.LMARGIN, new_y=YPos.NEXT) + pdf.cell(0, 10, f"Total Profit: {profit_data['sum_wins']}", new_x=XPos.LMARGIN, new_y=YPos.NEXT) + pdf.cell(0, 10, f"Total Loss: {profit_data['sum_losses']}", new_x=XPos.LMARGIN, new_y=YPos.NEXT) + best_trade_profit = max(profit_data["long_wins"], profit_data["short_wins"]) + pdf.cell(0, 10, f"Best Trade Profit: {best_trade_profit}", new_x=XPos.LMARGIN, new_y=YPos.NEXT) + worst_trade_profit = min(trade["profit"] for trade in prescr_trades_data) + pdf.cell(0, 10, f"Worst Trade Profit: {worst_trade_profit}", new_x=XPos.LMARGIN, new_y=YPos.NEXT) + + # Save PDF + pdf.output(output_file) + + if direct is False: + # Save PDF + pdf.output(output_file) + else: + # Instead of saving to a file, write to a BytesIO buffer + pdf_buffer = BytesIO() + pdf.output(pdf_buffer) + pdf_buffer.seek(0) # Move to the beginning of the BytesIO buffer + return 0, pdf_buffer + +# Example usage: +if __name__ == '__main__': + id = "c3e31cb5-ddf9-467e-a932-2118f6844355" + res, val = create_trading_report_pdf(id, True) + + print(res,val) diff --git a/v2realbot/reporting/metricstoolsimage.py b/v2realbot/reporting/metricstoolsimage.py new file mode 100644 index 0000000..d4cadf5 --- /dev/null +++ b/v2realbot/reporting/metricstoolsimage.py @@ -0,0 +1,382 @@ +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.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 +# Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere + +def generate_trading_report_image(runner_ids: list = None, batch_id: str = None, stream: bool = False): + + #TODO dopracovat drawdown a minimalni a maximalni profity nikoliv cumulovane, zamyslet se + #TODO list of runner_ids + #TODO pridelat na vytvoreni runnera a batche, samostatne REST API + na remove archrunnera + + if runner_ids is None and batch_id is None: + return -2, f"runner_id or batch_id must be present" + + 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" + + trades = [] + for id in runner_ids: + #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" + + print("archrunner") + print(sada) + + # Parse trades + #trades = [Trade(**trade_dict) for trade_dict in set.metrics["prescr_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) + trade_dict['entry_time'] = datetime.fromtimestamp(trade_dict.get('entry_time')).astimezone(zoneNY) + trade_dict['exit_time'] = datetime.fromtimestamp(trade_dict.get('exit_time')).astimezone(zoneNY) + trades.append(Trade(**trade_dict)) + + print(trades) + + # Filter to only use trades with status 'CLOSED' + closed_trades = [trade for trade in trades if trade.status == TradeStatus.CLOSED] + + # Data extraction for the plots + exit_times = [trade.exit_time for trade in closed_trades if trade.exit_time is not None] + cumulative_profits = [trade.profit_sum for trade in closed_trades if trade.profit_sum is not None] + profits = [trade.profit for trade in closed_trades if trade.profit is not None] + wins = [trade.profit for trade in closed_trades if trade.profit > 0] + losses = [trade.profit for trade in closed_trades if trade.profit < 0] + + wins_long = [trade.profit for trade in closed_trades if trade.profit > 0 and trade.direction == TradeDirection.LONG] + losses_long = [trade.profit for trade in closed_trades if trade.profit < 0 and trade.direction == TradeDirection.LONG] + wins_short = [trade.profit for trade in closed_trades if trade.profit > 0 and trade.direction == TradeDirection.SHORT] + losses_short = [trade.profit for trade in closed_trades if trade.profit < 0 and trade.direction == TradeDirection.SHORT] + + directions = [trade.direction for trade in closed_trades] + + long_profits = [trade.profit for trade in closed_trades if trade.direction == TradeDirection.LONG and trade.profit is not None] + short_profits = [trade.profit for trade in closed_trades if trade.direction == TradeDirection.SHORT and trade.profit is not None] + + # 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) + + # Create a combined figure for all plots + fig, axs = plt.subplots(3, 4, figsize=(11, 7)) + + #TITLE + title = "" + cnt_ids = len(runner_ids) + if batch_id is not None: + title = "Batch: "+str(batch_id)+ " " + + title += "Days: " + str(cnt_ids) + if cnt_ids == 1: + title += " ("+str(runner_ids[0])[0:14]+") " + + if sada.mode == Mode.BT: + datum = sada.bt_from + else: + datum = sada.started + + title += datum.strftime("%d.%m.%Y %H:%M") + + + # Add a title to the figure + fig.suptitle(title, fontsize=15, color='white') + + # Plot 1: Overall Profit Summary Chart + total_wins = int(sum(wins)) + total_losses = int(sum(losses)) + net_profit = int(sum(profits)) + sns.barplot(x=['Total', 'Wins','Losses'], + y=[net_profit, total_wins, total_losses], + ax=axs[0, 0]) + axs[0, 0].set_title('Overall Profit Summary') + # Define the offset for placing text inside the bars + offset = max(total_wins, abs(total_losses), net_profit) * 0.05 # 5% of the highest (or lowest) bar value + + # Function to place text annotation + def place_annotation(ax, x, value, offset): + va = 'top' if value >= 0 else 'bottom' + y = value - offset if value >= 0 else value + offset + ax.text(x, y, f'{value}', ha='center', va=va, color='black', fontsize=12) + + # Annotate the Total Wins, Losses, and Net Profit bars + place_annotation(axs[0, 0], 0, net_profit, offset) + place_annotation(axs[0, 0], 1, total_wins, offset) + place_annotation(axs[0, 0], 2, total_losses, offset) + + # Plot 2: LONG - profit summary + total_wins_long = int(sum(wins_long)) + total_losses_long = int(sum(losses_long)) + total_long = total_wins_long + total_losses_long + sns.barplot(x=['Total', 'Wins','Losses'], + y=[total_long, total_wins_long, total_losses_long], + ax=axs[0, 1]) + axs[0, 1].set_title('LONG Profit Summary') + # Define the offset for placing text inside the bars + offset = max(total_wins_long, abs(total_losses_long)) * 0.05 # 5% of the highest (or lowest) bar value + + place_annotation(axs[0, 1], 0, total_long, offset) + place_annotation(axs[0, 1], 1, total_wins_long, offset) + place_annotation(axs[0, 1], 2, total_losses_long, offset) + + + # Plot 3: SHORT - profit summary + total_wins_short =int(sum(wins_short)) + total_losses_short = int(sum(losses_short)) + total_short = total_wins_short + total_losses_short + sns.barplot(x=['Total', 'Wins', 'Losses'], + y=[total_short, total_wins_short, + total_losses_short], + ax=axs[0, 2]) + axs[0, 2].set_title('SHORT Profit Summary') + # Define the offset for placing text inside the bars + offset = max(total_wins_short, abs(total_losses_short)) * 0.05 # 5% of the highest (or lowest) bar value + + place_annotation(axs[0, 2], 0, total_short, offset) + place_annotation(axs[0, 2], 1, total_wins_short, offset) + place_annotation(axs[0, 2], 2, total_losses_short, offset) + + # Plot 4: Trade Counts Bar Chart + long_count = len([trade for trade in closed_trades if trade.direction == TradeDirection.LONG]) + short_count = len([trade for trade in closed_trades if trade.direction == TradeDirection.SHORT]) + sns.barplot(x=['Long Trades', 'Short Trades'], y=[long_count, short_count], ax=axs[0, 3]) + axs[0, 3].set_title('Trade Counts') + offset = max(long_count, short_count) * 0.05 # 5% of the highest (or lowest) bar value + + place_annotation(axs[0, 3], 0, long_count, offset) + place_annotation(axs[0, 3], 1, short_count, offset) + + + #Cumulative profit - bud 1 den nebo vice dni + if len(runner_ids)== 1: + # Plot 3: Cumulative Profit Over Time with Max Profit Point + max_profit_time = exit_times[np.argmax(cumulative_profits)] + max_profit = max(cumulative_profits) + min_profit_time = exit_times[np.argmin(cumulative_profits)] + min_profit = min(cumulative_profits) + sns.lineplot(x=exit_times, y=cumulative_profits, label='Cumulative Profit', ax=axs[1, 3]) + axs[1, 3].scatter(max_profit_time, max_profit, color='green', label='Max Profit') + axs[1, 3].scatter(min_profit_time, min_profit, color='red', label='Min Profit') + # Format dates on the x-axis + axs[1, 3].xaxis.set_major_formatter(mdates.DateFormatter('%H', tz=zoneNY)) + axs[1, 3].set_title('Cumulative Profit Over Time') + axs[1, 3].legend() + else: + # Calculate cumulative profit + # Additional Plot: Cumulative Profit Over Time + # Sort trades by exit time + sorted_trades = sorted([trade for trade in trades if trade.status == TradeStatus.CLOSED], + key=lambda x: x.exit_time) + cumulative_profits = np.cumsum([trade.profit for trade in sorted_trades]) + exit_times_sorted = [trade.exit_time for trade in sorted_trades] + axs[1, 3].plot(exit_times_sorted, cumulative_profits, color='blue') + axs[1, 3].set_title('Cumulative Profit Over Time') + axs[1, 3].set_xlabel('Time') + axs[1, 3].set_ylabel('Cumulative Profit') + axs[1, 3].xaxis.set_major_formatter(mdates.DateFormatter('%d', tz=zoneNY)) + + # Creating a DataFrame for the heatmap + heatmap_data_list = [] + for trade in trades: + if trade.status == TradeStatus.CLOSED: + day = trade.exit_time.strftime('%m-%d') # Format date as 'MM-DD' + #day = trade.exit_time.date() + hour = trade.exit_time.hour + profit = trade.profit + heatmap_data_list.append({'Day': day, 'Hour': hour, 'Profit': profit}) + + heatmap_data = pd.DataFrame(heatmap_data_list) + heatmap_data = heatmap_data.groupby(['Day', 'Hour']).sum().reset_index() + heatmap_pivot = heatmap_data.pivot(index='Day', columns='Hour', values='Profit') + + # Plot 3: Heatmap of Profits + sns.heatmap(heatmap_pivot, cmap='viridis', ax=axs[1, 0]) + axs[1, 0].set_title('Heatmap of Profits (based on Exit time)') + axs[1, 0].set_xlabel('Hour of Day') + axs[1, 0].set_ylabel('Day') + + # Plot 9: Profit/Loss Distribution Histogram + sns.histplot(profits, bins=30, ax=axs[1, 1], kde=True, color='skyblue') + axs[1, 1].set_title('Profit/Loss Distribution') + axs[1, 1].set_xlabel('Profit/Loss') + axs[1, 1].set_ylabel('Frequency') + + # Plot 5 + # - pro 1 den: Position Size Distribution + # - pro vice dnu: Trade Duration vs. Profit/Loss + if len(runner_ids) == 1: + + sizes = [trade.size for trade in closed_trades if trade.size is not None] + size_counts = {size: sizes.count(size) for size in set(sizes)} + sns.barplot(x=list(size_counts.keys()), y=list(size_counts.values()), ax=axs[1, 2]) + axs[1, 2].set_title('Position Size Distribution') + else: + trade_durations = [] + trade_profits = [] + #trade_volumes = [] # Assuming you have a way to measure the size/volume of each trade + trade_types = [] # 'Long' or 'Short' + + for trade in trades: + if trade.status == TradeStatus.CLOSED: + duration = (trade.exit_time - trade.entry_time).total_seconds() / 60 # Duration in minutes (3600 for hours) + trade_durations.append(duration) + trade_profits.append(trade.profit) + ##trade_volumes.append(trade.size) # or any other measure of trade size + trade_types.append('Long' if trade.direction == TradeDirection.LONG else 'Short') + + # Plot 8: Trade Duration vs. Profit/Loss + scatter_data = pd.DataFrame({ + 'Duration': trade_durations, + 'Profit': trade_profits, + #'Volume': trade_volumes, + 'Type': trade_types + }) + #sns.scatterplot(data=scatter_data, x='Duration', y='Profit', size='Volume', hue='Type', ax=axs[1, 2]) + sns.scatterplot(data=scatter_data, x='Duration', y='Profit', hue='Type', ax=axs[1, 2]) + axs[1, 2].set_title('Trade Duration vs. Profit/Loss') + axs[1, 2].set_xlabel('Duration (Minutes)') + axs[1, 2].set_ylabel('Profit/Loss') + + + # Plot 6: Daily Relative Profit Chart + if len(runner_ids) == 1: + daily_rel_profits = [trade.rel_profit for trade in closed_trades if trade.rel_profit is not None] + sns.lineplot(x=range(len(daily_rel_profits)), y=daily_rel_profits, ax=axs[2, 0]) + axs[2, 0].set_title('Daily Relative Profit') + else: + # Creating a DataFrame for the heatmap + heatmap_data_list = [] + for trade in trades: + if trade.status == TradeStatus.CLOSED: + day = trade.entry_time.strftime('%m-%d') # Format date as 'MM-DD' + #day = trade.entry_time.date() + hour = trade.entry_time.hour + profit = trade.profit + heatmap_data_list.append({'Day': day, 'Hour': hour, 'Profit': profit}) + + heatmap_data = pd.DataFrame(heatmap_data_list) + heatmap_data = heatmap_data.groupby(['Day', 'Hour']).sum().reset_index() + heatmap_pivot = heatmap_data.pivot(index='Day', columns='Hour', values='Profit') + + # Plot 3: Heatmap of Profits + sns.heatmap(heatmap_pivot, cmap='viridis', ax=axs[2, 0]) + axs[2, 0].set_title('Heatmap of Profits (based on Entry time)') + axs[2, 0].set_xlabel('Hour of Day') + axs[2, 0].set_ylabel('Day') + + # Plot 8: Profits Based on Hour of the Day (Entry) + entry_hours = [trade.entry_time.hour for trade in closed_trades if trade.entry_time is not None] + profits_by_hour = {} + for hour, trade in zip(entry_hours, closed_trades): + if hour not in profits_by_hour: + profits_by_hour[hour] = 0 + profits_by_hour[hour] += trade.profit + + # Sorting by hour for plotting + sorted_hours = sorted(profits_by_hour.keys()) + sorted_profits = [profits_by_hour[hour] for hour in sorted_hours] + + sns.barplot(x=sorted_hours, y=sorted_profits, ax=axs[2, 1]) + axs[2, 1].set_title('Profits by Hour of Day (Entry)') + axs[2, 1].set_xlabel('Hour of Day') + axs[2, 1].set_ylabel('Profit') + + # Plot 9: Profits Based on Hour of the Day - based on Exit + exit_hours = [trade.exit_time.hour for trade in closed_trades if trade.exit_time is not None] + profits_by_hour = {} + for hour, trade in zip(exit_hours, closed_trades): + if hour not in profits_by_hour: + profits_by_hour[hour] = 0 + profits_by_hour[hour] += trade.profit + + # Sorting by hour for plotting + sorted_hours = sorted(profits_by_hour.keys()) + sorted_profits = [profits_by_hour[hour] for hour in sorted_hours] + + sns.barplot(x=sorted_hours, y=sorted_profits, ax=axs[2, 2]) + axs[2, 2].set_title('Profits by Hour of Day (Exit)') + axs[2, 2].set_xlabel('Hour of Day') + axs[2, 2].set_ylabel('Profit') + + # Calculate profits by day of the week + day_of_week_profits = {i: 0 for i in range(7)} # Dictionary to store profits for each day of the week + + for trade in trades: + if trade.status == TradeStatus.CLOSED: + day_of_week = trade.exit_time.weekday() # Monday is 0 and Sunday is 6 + day_of_week_profits[day_of_week] += trade.profit + + days = ['Mo', 'Tue', 'Wed', 'Thu', 'Fri'] + # Additional Plot: Strategy Performance by Day of the Week + axs[2, 3].bar(days, [day_of_week_profits[i] for i in range(5)]) + axs[2, 3].set_title('Profit by Day of the Week') + axs[2, 3].set_xlabel('Day of the Week') + axs[2, 3].set_ylabel('Cumulative Profit') + + #filename + file = batch_id if batch_id is not None else runner_ids[0] + image_file_name = f"{file}.png" + image_path = str(MEDIA_DIRECTORY / "basic" / image_file_name) + + # Adjust layout and save the combined plot as an image + plt.tight_layout() + + if stream is False: + plt.savefig(image_path) + plt.close() + 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 + +# Example usage +# trades = [list of Trade objects] +if __name__ == '__main__': + id_list = ["c3e31cb5-ddf9-467e-a932-2118f6844355"] + generate_trading_report_image(runner_ids=id_list) + # batch_id = "90973e57" + # generate_trading_report_image(batch_id=batch_id) diff --git a/v2realbot/static/index.html b/v2realbot/static/index.html index bf18666..63f9e6d 100644 --- a/v2realbot/static/index.html +++ b/v2realbot/static/index.html @@ -279,7 +279,9 @@ - +
+ +
diff --git a/v2realbot/static/js/archivetables.js b/v2realbot/static/js/archivetables.js index 2dbb909..d16036d 100644 --- a/v2realbot/static/js/archivetables.js +++ b/v2realbot/static/js/archivetables.js @@ -110,9 +110,61 @@ function prepare_export() { return trdList } +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(); + }; + img.onerror = function() { + console.log("no image available") + // If the image fails to load, do nothing + }; +} + $(document).ready(function () { archiveRecords.ajax.reload(); + // 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) { + //var imageUrl = '/media/report_'+data.id+".png"; // Replace with your logic to get image URL + var imageUrl = '/media/basic/'+data.batch_id+'.png'; // Replace with your logic to get image URL + console.log(imageUrl) + display_image(imageUrl) + } + }); + + // $('#archiveTable tbody').on('mouseleave', 'td:nth-child(2)', function () { + // $('#imagePreview').hide(); + // }); + + // Hide image on click anywhere in the document + $(document).on('click', function() { + $('#imagePreview').hide(); + }); + + function hideImage() { + $('#imagePreview').hide(); + } + // $('#archiveTable tbody').on('mousemove', 'td:nth-child(2)', function(e) { + // $('#imagePreview').css({'top': e.pageY + 10, 'left': e.pageX + 10}); + // }); + + //button export $('#button_export_xml').click(function () { xmled = convertToXml(prepare_export()) @@ -360,6 +412,46 @@ $(document).ready(function () { } }); + //generate report button + $('#button_report').click(function () { + rows = archiveRecords.rows('.selected'); + if (rows == undefined) { + return + } + 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)) + }, + error: function(xhr, status, error) { + console.log("proc to skace do erroru?") + //window.alert(JSON.stringify(xhr)); + console.log(JSON.stringify(xhr)); + } + }) + }); + //delete button $('#button_delete_arch').click(function () { diff --git a/v2realbot/static/main.css b/v2realbot/static/main.css index 1f2cf1f..16687d7 100644 --- a/v2realbot/static/main.css +++ b/v2realbot/static/main.css @@ -164,7 +164,14 @@ table.dataTable thead>tr>th.sorting_asc:before, table.dataTable thead>tr>th.sort --bs-gradient: none; } - +#imagePreview { + display: none; + position: fixed; + z-index: 100; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} /* .btn-outline-success { --bs-btn-color: #316164; diff --git a/v2realbot/strategy/StrategyClassicSL.py b/v2realbot/strategy/StrategyClassicSL.py index 04b0c3c..a9e3d6f 100644 --- a/v2realbot/strategy/StrategyClassicSL.py +++ b/v2realbot/strategy/StrategyClassicSL.py @@ -129,14 +129,27 @@ class StrategyClassicSL(Strategy): #pokud jde o finalni FILL - pridame do pole tento celkovy relativnich profit (ze ktereho se pocita kumulativni relativni profit) rel_profit_cum_calculated = 0 - + partial_exit = False + partial_last = False if data.event == TradeEvent.FILL: - #TODO pokud mame partial exit, tak se spravne vypocita relativni profit, ale - # je jen na mensi mnozszvi take z nej delat cum_calculate je blbost - OPRAVIT - self.state.rel_profit_cum.append(rel_profit) - rel_profit_cum_calculated = round(np.sum(self.state.rel_profit_cum),5) + #jde o partial EXIT dvááme si rel.profit do docasne promenne, po poslednim exitu z nich vypocteme skutecny rel.profit + if data.position_qty != 0: + self.state.docasny_rel_profit.append(rel_profit) + partial_exit = True + else: + #jde o posledni z PARTIAL EXITU tzn.data.position_qty == 0 + if len(self.state.docasny_rel_profit) > 0: + #pricteme aktualni rel profit + self.state.docasny_rel_profit.append(rel_profit) + #a z rel profitu tohoto tradu vypocteme prumer, ktery teprve ulozime + rel_profit = round(np.mean(self.state.docasny_rel_profit),5) + self.state.docasny_rel_profit = [] + partial_last = True - self.state.ilog(e=f"BUY notif - SHORT PROFIT:{round(float(trade_profit),3)} celkem:{round(float(self.state.profit),3)} rel:{float(rel_profit)} rel_cum:{round(rel_profit_cum_calculated,7)}", msg=str(data.event), rel_profit_cum=str(self.state.rel_profit_cum), bought_amount=bought_amount, avg_costs=avg_costs, trade_qty=data.qty, trade_price=data.price, orderid=str(data.order.id)) + self.state.rel_profit_cum.append(rel_profit) + rel_profit_cum_calculated = round(np.sum(self.state.rel_profit_cum),5) + + self.state.ilog(e=f"BUY notif - SHORT PROFIT: {partial_exit=} {partial_last=} {round(float(trade_profit),3)} celkem:{round(float(self.state.profit),3)} rel:{float(rel_profit)} rel_cum:{round(rel_profit_cum_calculated,7)}", msg=str(data.event), rel_profit_cum=str(self.state.rel_profit_cum), bought_amount=bought_amount, avg_costs=avg_costs, trade_qty=data.qty, trade_price=data.price, orderid=str(data.order.id)) #zapsat profit do prescr.trades for trade in self.state.vars.prescribedTrades: @@ -260,12 +273,27 @@ class StrategyClassicSL(Strategy): rel_profit = round((trade_profit / (vstup_cena * float(data.order.qty))) * 100,5) rel_profit_cum_calculated = 0 - #pokud jde o finalni FILL - pridame do pole relativnich profit (ze ktereho se pocita kumulativni relativni profit) + partial_exit = False + partial_last = False if data.event == TradeEvent.FILL: - self.state.rel_profit_cum.append(rel_profit) - rel_profit_cum_calculated = round(np.sum(self.state.rel_profit_cum),5) + #jde o partial EXIT dvááme si rel.profit do docasne promenne, po poslednim exitu z nich vypocteme skutecny rel.profit + if data.position_qty != 0: + self.state.docasny_rel_profit.append(rel_profit) + partial_exit = True + else: + #jde o posledni z PARTIAL EXITU tzn.data.position_qty == 0 + if len(self.state.docasny_rel_profit) > 0: + #pricteme aktualni rel profit + self.state.docasny_rel_profit.append(rel_profit) + #a z rel profitu tohoto tradu vypocteme prumer, ktery teprve ulozime + rel_profit = round(np.mean(self.state.docasny_rel_profit),5) + self.state.docasny_rel_profit = [] + partial_last = True - self.state.ilog(e=f"SELL notif - PROFIT:{round(float(trade_profit),3)} celkem:{round(float(self.state.profit),3)} rel:{float(rel_profit)} rel_cum:{round(rel_profit_cum_calculated,7)}", msg=str(data.event), rel_profit_cum = str(self.state.rel_profit_cum), sold_amount=sold_amount, avg_costs=avg_costs, trade_qty=data.qty, trade_price=data.price, orderid=str(data.order.id)) + self.state.rel_profit_cum.append(rel_profit) + rel_profit_cum_calculated = round(np.sum(self.state.rel_profit_cum),5) + + self.state.ilog(e=f"SELL notif - LONG PROFIT {partial_exit=} {partial_last=}:{round(float(trade_profit),3)} celkem:{round(float(self.state.profit),3)} rel:{float(rel_profit)} rel_cum:{round(rel_profit_cum_calculated,7)}", msg=str(data.event), rel_profit_cum = str(self.state.rel_profit_cum), sold_amount=sold_amount, avg_costs=avg_costs, trade_qty=data.qty, trade_price=data.price, orderid=str(data.order.id)) #zapsat profit do prescr.trades for trade in self.state.vars.prescribedTrades: diff --git a/v2realbot/strategy/base.py b/v2realbot/strategy/base.py index c1b721e..64f16c4 100644 --- a/v2realbot/strategy/base.py +++ b/v2realbot/strategy/base.py @@ -707,6 +707,7 @@ class StrategyState: self.runner_id = runner_id self.bt = bt self.dont_exit_already_activated = False + self.docasny_rel_profit = [] self.ilog_save = ilog_save self.sl_optimizer_short = optimsl.SLOptimizer(ptm.TradeDirection.SHORT) self.sl_optimizer_long = optimsl.SLOptimizer(ptm.TradeDirection.LONG) diff --git a/v2realbot/strategyblocks/indicators/helpers.py b/v2realbot/strategyblocks/indicators/helpers.py index 5c421cd..7e1c574 100644 --- a/v2realbot/strategyblocks/indicators/helpers.py +++ b/v2realbot/strategyblocks/indicators/helpers.py @@ -82,7 +82,10 @@ def get_source_series(state, source: str): try: return state.bars[source] except KeyError: - return state.indicators[source] + try: + return state.indicators[source] + except KeyError: + return None else: dict_name = source[:split_index] key = source[split_index + 1:] diff --git a/v2realbot/strategyblocks/newtrade/signals.py b/v2realbot/strategyblocks/newtrade/signals.py index b9b1040..71f7320 100644 --- a/v2realbot/strategyblocks/newtrade/signals.py +++ b/v2realbot/strategyblocks/newtrade/signals.py @@ -7,6 +7,7 @@ from datetime import datetime from rich import print as printanyway from traceback import format_exc from v2realbot.strategyblocks.newtrade.conditions import go_conditions_met, common_go_preconditions_check +from v2realbot.strategyblocks.newtrade.sizing import get_size, get_multiplier def signal_search(state: StrategyState, data): # SIGNAL sekce ve stratvars obsahuje signaly: Ty se skladaji z obecnych parametru a podsekce podminek. @@ -42,7 +43,7 @@ def execute_signal_generator(state, data, name): options = safe_get(state.vars.signals, name, None) if options is None: - state.ilog(lvl=1,e="No options for {name} in stratvars") + state.ilog(lvl=1,e=f"No options for {name} in stratvars") return if common_go_preconditions_check(state, data, signalname=name, options=options) is False: @@ -71,20 +72,26 @@ def execute_signal_generator(state, data, name): if long_enabled is False: state.ilog(lvl=1,e=f"{name} LONG DISABLED") if long_enabled and go_conditions_met(state, data,signalname=name, direction=TradeDirection.LONG): + multiplier = get_multiplier(state, data, options, TradeDirection.LONG) state.vars.prescribedTrades.append(Trade( id=uuid4(), last_update=datetime.fromtimestamp(state.time).astimezone(zoneNY), status=TradeStatus.READY, generated_by=name, + size=multiplier*state.vars.chunk, + size_multiplier = multiplier, direction=TradeDirection.LONG, entry_price=None, stoploss_value = None)) elif short_enabled and go_conditions_met(state, data, signalname=name, direction=TradeDirection.SHORT): + multiplier = get_multiplier(state, data, options, TradeDirection.SHORT) state.vars.prescribedTrades.append(Trade( id=uuid4(), last_update=datetime.fromtimestamp(state.time).astimezone(zoneNY), status=TradeStatus.READY, generated_by=name, + size=multiplier*state.vars.chunk, + size_multiplier = multiplier, direction=TradeDirection.SHORT, entry_price=None, stoploss_value = None)) diff --git a/v2realbot/strategyblocks/newtrade/sizing.py b/v2realbot/strategyblocks/newtrade/sizing.py new file mode 100644 index 0000000..a6a76e6 --- /dev/null +++ b/v2realbot/strategyblocks/newtrade/sizing.py @@ -0,0 +1,143 @@ +from v2realbot.strategy.base import StrategyState +from v2realbot.common.PrescribedTradeModel import Trade, TradeDirection, TradeStatus +import v2realbot.utils.utils as utls +from v2realbot.config import KW +from uuid import uuid4 +from datetime import datetime +from rich import print as printanyway +from traceback import format_exc +from v2realbot.strategyblocks.newtrade.conditions import go_conditions_met, common_go_preconditions_check +from v2realbot.strategyblocks.indicators.helpers import get_source_series +import numpy as np + +def get_size(state: StrategyState, data, signaloptions: dict, direction: TradeDirection): + return state.vars.chunk * get_multiplier(state, signaloptions, direction) + +def get_multiplier(state: StrategyState, data, signaloptions: dict, direction: TradeDirection): + """" + Function return dynamic sizing multiplier according to directive and current trades. + + Default: state.vars.chunk + + Additional sizing logic is layered on top of each other according to directives. + + Currently supporting: + 1) pattern sizing U-shape, hat-shape (x - bars, minutes, y - sizing multiplier 0 to 1 ) + 2) probes + + Future ideas: + - ML sizing model + + DIRECTIVES: + #sondy na zacatku market + probe_enabled = true # sonda zapnuta + number = 1 # pocet sond + probe_size = 0.01 #velikost sondy, nasobek def size + + #pattern - dynamicka uprava na zaklade casu + pattern_enabled = true + pattern_source = "minutes" #or any series - indicators, bars or state etc. (index[-1], np.sum(state.rel_profit_cum)...) + pattern_source_vals = [0,30,90, 200, 300, 390] + pattern_sizing_vals = [0.1,0.5, 0.8, 1, 0.6, 0.1] + + #np.interp(atr10, [0.001, 0.06], [1, 5]) + #size_multiplier = np.interp(pattern_source, [SIZING_pattern_source_vals], [SIZING_pattern_sizing_vals]) + + + #TODO + - pomocna graf pro vizualizaci interpolace - v tools/sizingpatternvisual.py + - dopsat do dokumentace direktiv - do tabulky + - ukládat sizing coeff do prescrTrades + - upravit výpočet denního relativniho profitu u tradu na základě vstupního sizing koeficientu + - vyresit zda max_oss_to_quit_rel aplikovat bud per alokovana pozice nebo trade + (pokud mam na trade, pak mi zafunguje i na minimalni sodnu) Zatim bude + realizován takto + + - nejprve ověří rel profit tradu a pokud přesáhne, strategie se suspendne + - poté se rel profit tradu vynásobí multiplikátorem a započte se do denního rel profitu, jehož + výše se následně také ověří + + NOTE: zatim neupraveno, a do denniho rel profitu se zapocitava plnym pomerem, diky tomu + si muzu dat i na sondu -0.5 suspend strategie. Nevyhoda: rel.profit presne neodpovida + + [stratvars.signals.morning1.sizing] #specificke pro dany signal + probe_enabled = true + probe_size = 0.01 + pattern_enabled = true + # pattern_source = "minutes" #or any series - indicators, bars or state etc. (index[-1], np.sum(state.rel_profit_cum)...) + pattern_source_axis = [0,30,90, 200, 300, 390] + pattern_size_axis = [0.1,0.5, 0.8, 1, 0.6, 0.1] + + [stratvars.sizing] #obecne jako fallback pro vsechny signaly + probe_enabled = true + probe_size = 0.01 + pattern_enabled = true + # pattern_source = "minutes" #or any series - indicators, bars or state etc. (index[-1], np.sum(state.rel_profit_cum)...) + pattern_source_axis = [0,30,90, 200, 300, 390] + pattern_size_axis = [0.1,0.5, 0.8, 1, 0.6, 0.1] + + """"" + multiplier = 1 + + #fallback common sizing sekci + fallback_options = utls.safe_get(state.vars, 'sizing', None) + + #signal specific sekce + options = utls.safe_get(signaloptions, 'sizing', fallback_options) + + if options is None: + state.ilog(lvl=1,e="No sizing options common or signal specific in stratvars") + return multiplier + + #PROBE ENABLED + # probe_enabled = true # sonda zapnuta + # probe_number = 1 # pocet sond + # probe_size = 0.01 #velikost sondy, nasobek def size + + probe_enabled = utls.safe_get(options, "probe_enabled", False) + + if probe_enabled: + #zatim pouze probe number 1 natvrdo, tzn. nesmi byt trade pro aktivace + if state.vars.last_in_index is None: + #probe_number = utls.safe_get(options, "probe_number",1) + probe_size = float(utls.safe_get(options, "probe_size", 0.1)) + state.ilog(lvl=1,e=f"SIZER - PROBE - setting multiplier to {probe_size}", options=options) + return probe_size + + #SIZING PATTER + # pattern_enabled = true + # pattern_source = "minutes" #or any series - indicators, bars or state etc. (index[-1], np.sum(state.rel_profit_cum)...) + # pattern_source_axis = [0,30,90, 200, 300, 390] + # pattern_size_axis = [0.1,0.5, 0.8, 1, 0.6, 0.1] + pattern_enabled = utls.safe_get(options, "pattern_enabled", False) + + if pattern_enabled: + input_value = None + pattern_source = utls.safe_get(options, "pattern_source", "minutes") + + #TODO do budoucna mozna sem dát libovolnou series např. index, time, profit, rel_profit? + if pattern_source != "minutes": + + input_value = eval(pattern_source, {'state': state, 'np': np, 'utls': utls}, state.ind_mapping) + + if input_value is None: + state.ilog(lvl=1,e=f"SIZER - ERROR Pattern source is None, after evaluation of expression", options=str(options)) + return multiplier + else: + input_value = utls.minutes_since_market_open(datetime.fromtimestamp(data['updated']).astimezone(utls.zoneNY)) + + pattern_source_axis = utls.safe_get(options, "pattern_source_axis", None) + pattern_size_axis = utls.safe_get(options, "pattern_size_axis", None) + + if pattern_source_axis is None or pattern_size_axis is None: + state.ilog(lvl=1,e=f"SIZER - Pattern source and size axis must be set", options=str(options)) + return multiplier + + state.ilog(lvl=1,e=f"SIZER - Input value of {pattern_source} value {input_value}", options=options, time=state.time) + multiplier = np.interp(input_value, pattern_source_axis, pattern_size_axis) + state.ilog(lvl=1,e=f"SIZER - Interpolated value {multiplier}", input_value=input_value, pattern_source_axis=pattern_source_axis, pattern_size_axis=pattern_size_axis, options=options, time=state.time) + + if multiplier > 1 or multiplier <= 0: + state.ilog(lvl=1,e=f"SIZER - Mame nekde problem MULTIPLIER mimo RANGE ERROR {multiplier}", options=options, time=state.time) + multiplier = 1 + return multiplier diff --git a/v2realbot/tools/sizingpattervisual.py b/v2realbot/tools/sizingpattervisual.py new file mode 100644 index 0000000..b5716a5 --- /dev/null +++ b/v2realbot/tools/sizingpattervisual.py @@ -0,0 +1,29 @@ +import numpy as np +import matplotlib.pyplot as plt + +# Sem zadat pattern X a Y pro VIZUALIZACI + +#minutes +pattern_x = [0, 30, 90, 200, 300, 390] +pattern_y = [0.1, 0.5, 0.8, 1, 0.6, 0.1] + +#bar index - použitelné u time scale barů +pattern_x = [0, 30, 90, 200, 300, 390] +pattern_y = [0.1, 0.5, 0.8, 1, 0.6, 0.1] + +#celkový profit + +# Generating a range of input values for interpolation +input_values = np.linspace(min(pattern_x), max(pattern_x), 500) +multipliers = np.interp(input_values, pattern_x, pattern_y) + +# Plotting +plt.figure(figsize=(10, 6)) +plt.plot(pattern_x, pattern_y, 'o', label='Original Points') +plt.plot(input_values, multipliers, label='Interpolated Values') +plt.xlabel('X values') +plt.ylabel('Interpolated Multipliers') +plt.title('Interpolation Chart') +plt.legend() +plt.grid(True) +plt.show() diff --git a/v2realbot/utils/utils.py b/v2realbot/utils/utils.py index 5bc2a24..d2cc6f1 100644 --- a/v2realbot/utils/utils.py +++ b/v2realbot/utils/utils.py @@ -464,6 +464,30 @@ def is_open_rush(dt: datetime, mins: int = 30): rushtime = (datetime.combine(date.today(), business_hours["from"]) + timedelta(minutes=mins)).time() return business_hours["from"] <= dt.time() < rushtime + +#TODO v budoucnu predelat - v initu nacist jednou market open cas a ten pouzivat vsude +#kde je treba (ted je tady natvrdo 9.30) +def minutes_since_market_open(datetime_aware: datetime): + """ + Calculate the number of minutes elapsed from 9:30 AM to the given timezone-aware datetime of the same day. + This version is optimized for speed and should be used when calling in a loop. + + :param datetime_aware: A timezone-aware datetime object representing the time to compare. + :return: The number of minutes since today's 9:30 AM. + """ + # Ensure the input datetime is timezone-aware + if datetime_aware.tzinfo is None or datetime_aware.tzinfo.utcoffset(datetime_aware) is None: + raise ValueError("The input datetime must be timezone-aware.") + + # Calculate minutes since midnight for both times + minutes_since_midnight = datetime_aware.hour * 60 + datetime_aware.minute + morning_minutes = 9 * 60 + 30 + + # Calculate the difference + delta_minutes = minutes_since_midnight - morning_minutes + + return delta_minutes if delta_minutes >= 0 else 0 + #optimalized by BARD def is_window_open(dt: datetime, start: int = 0, end: int = 390): """"