sizing, progressionbar,reporting basics
This commit is contained in:
5
media/basic/.gitignore
vendored
Normal file
5
media/basic/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Ignore everything in this directory
|
||||||
|
*
|
||||||
|
|
||||||
|
# Except this file
|
||||||
|
!.gitignore
|
||||||
@ -30,6 +30,8 @@ class Trade(BaseModel):
|
|||||||
entry_price: Optional[float] = None
|
entry_price: Optional[float] = None
|
||||||
goal_price: Optional[float] = None
|
goal_price: Optional[float] = None
|
||||||
size: Optional[int] = None
|
size: Optional[int] = None
|
||||||
|
# size_multiplier je pomocna promenna pro pocitani relativniho denniho profit
|
||||||
|
size_multiplier: Optional[float] = None
|
||||||
# stoploss_type: TradeStoplossType
|
# stoploss_type: TradeStoplossType
|
||||||
stoploss_value: Optional[float] = None
|
stoploss_value: Optional[float] = None
|
||||||
profit: Optional[float] = 0
|
profit: Optional[float] = 0
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
from alpaca.data.enums import DataFeed
|
from alpaca.data.enums import DataFeed
|
||||||
from v2realbot.enums.enums import Mode, Account, FillCondition
|
from v2realbot.enums.enums import Mode, Account, FillCondition
|
||||||
from appdirs import user_data_dir
|
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',
|
#'0.0.0.0',
|
||||||
#currently only prod server has acces to LIVE
|
#currently only prod server has acces to LIVE
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from v2realbot.utils.ilog import delete_logs
|
|||||||
from v2realbot.common.PrescribedTradeModel import Trade, TradeDirection, TradeStatus, TradeStoplossType
|
from v2realbot.common.PrescribedTradeModel import Trade, TradeDirection, TradeStatus, TradeStoplossType
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from threading import Thread, current_thread, Event, enumerate
|
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
|
import importlib
|
||||||
from alpaca.trading.requests import GetCalendarRequest
|
from alpaca.trading.requests import GetCalendarRequest
|
||||||
from alpaca.trading.client import TradingClient
|
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.inits.init_indicators import initialize_dynamic_indicators
|
||||||
from v2realbot.strategyblocks.indicators.indicators_hub import populate_dynamic_indicators
|
from v2realbot.strategyblocks.indicators.indicators_hub import populate_dynamic_indicators
|
||||||
from v2realbot.interfaces.backtest_interface import BacktestInterface
|
from v2realbot.interfaces.backtest_interface import BacktestInterface
|
||||||
|
import os
|
||||||
|
from v2realbot.reporting.metricstoolsimage import generate_trading_report_image
|
||||||
|
|
||||||
#from pyinstrument import Profiler
|
#from pyinstrument import Profiler
|
||||||
#adding lock to ensure thread safety of TinyDB (in future will be migrated to proper db)
|
#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)
|
archive_runner(runner=i, strat=target, inter_batch_params=inter_batch_params)
|
||||||
#mazeme runner po skonceni instance
|
#mazeme runner po skonceni instance
|
||||||
db.runners.remove(i)
|
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")
|
print("Runner STOPPED")
|
||||||
|
|
||||||
@ -500,6 +508,13 @@ def batch_run_manager(id: UUID, runReq: RunRequest, rundays: list[RunDay]):
|
|||||||
#i.history += str(runner.__dict__)+"<BR>"
|
#i.history += str(runner.__dict__)+"<BR>"
|
||||||
db.save()
|
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
|
#stratin run
|
||||||
def run_stratin(id: UUID, runReq: RunRequest, synchronous: bool = False, inter_batch_params: dict = None):
|
def run_stratin(id: UUID, runReq: RunRequest, synchronous: bool = False, inter_batch_params: dict = None):
|
||||||
if runReq.mode == Mode.BT:
|
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_profit"] = int(inter_batch_params["batch_profit"])
|
||||||
res["profit"]["batch_sum_rel_profit"] = inter_batch_params["batch_rel_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
|
#metrikz z prescribedTrades, pokud existuji
|
||||||
try:
|
try:
|
||||||
long_profit = 0
|
long_profit = 0
|
||||||
@ -696,39 +705,53 @@ def populate_metrics_output_directory(strat: StrategyInstance, inter_batch_param
|
|||||||
max_loss_time = None
|
max_loss_time = None
|
||||||
long_cnt = 0
|
long_cnt = 0
|
||||||
short_cnt = 0
|
short_cnt = 0
|
||||||
|
sum_wins_profit= 0
|
||||||
|
sum_loss = 0
|
||||||
|
|
||||||
if "prescribedTrades" in strat.state.vars:
|
if "prescribedTrades" in strat.state.vars:
|
||||||
for trade in strat.state.vars.prescribedTrades:
|
for trade in strat.state.vars.prescribedTrades:
|
||||||
if trade.profit_sum < max_loss:
|
if trade.status == TradeStatus.CLOSED:
|
||||||
max_loss = trade.profit_sum
|
if trade.profit_sum < max_loss:
|
||||||
max_loss_time = trade.last_update
|
max_loss = trade.profit_sum
|
||||||
if trade.profit_sum > max_profit:
|
max_loss_time = trade.last_update
|
||||||
max_profit = trade.profit_sum
|
if trade.profit_sum > max_profit:
|
||||||
max_profit_time = trade.last_update
|
max_profit = trade.profit_sum
|
||||||
if trade.status == TradeStatus.ACTIVATED and trade.direction == TradeDirection.LONG:
|
max_profit_time = trade.last_update
|
||||||
long_cnt += 1
|
if trade.direction == TradeDirection.LONG:
|
||||||
if trade.profit is not None:
|
long_cnt += 1
|
||||||
long_profit += trade.profit
|
if trade.profit is not None:
|
||||||
if trade.profit < 0:
|
long_profit += trade.profit
|
||||||
long_losses += trade.profit
|
if trade.profit < 0:
|
||||||
if trade.profit > 0:
|
long_losses += trade.profit
|
||||||
long_wins += trade.profit
|
if trade.profit > 0:
|
||||||
if trade.status == TradeStatus.ACTIVATED and trade.direction == TradeDirection.SHORT:
|
long_wins += trade.profit
|
||||||
short_cnt +=1
|
if trade.direction == TradeDirection.SHORT:
|
||||||
if trade.profit is not None:
|
short_cnt +=1
|
||||||
short_profit += trade.profit
|
if trade.profit is not None:
|
||||||
if trade.profit < 0:
|
short_profit += trade.profit
|
||||||
short_losses += trade.profit
|
if trade.profit < 0:
|
||||||
if trade.profit > 0:
|
short_losses += trade.profit
|
||||||
short_wins += 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"]["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"]["long_profit"] = round(long_profit,2)
|
||||||
res["profit"]["short_profit"] = round(short_profit,2)
|
res["profit"]["short_profit"] = round(short_profit,2)
|
||||||
res["profit"]["max_profit"] = round(max_profit,2)
|
#maximalni kumulativni profit (tzn. peaky profitu)
|
||||||
res["profit"]["max_profit_time"] = str(max_profit_time)
|
res["profit"]["max_profit_cum"] = round(max_profit,2)
|
||||||
res["profit"]["max_loss"] = round(max_loss,2)
|
res["profit"]["max_profit_cum_time"] = str(max_profit_time)
|
||||||
res["profit"]["max_loss_time"] = str(max_loss_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_wins"] = round(long_wins,2)
|
||||||
res["profit"]["long_losses"] = round(long_losses,2)
|
res["profit"]["long_losses"] = round(long_losses,2)
|
||||||
res["profit"]["short_wins"] = round(short_wins,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"
|
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
|
##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
|
#vlozeni celeho listu
|
||||||
res["prescr_trades"]=json.loads(json.dumps(strat.state.vars.prescribedTrades, default=json_serial))
|
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)
|
print(errmsg)
|
||||||
return -2, 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
|
#delete runner in archive and archive detail and runner logs
|
||||||
#predelano do JEDNE TRANSAKCE
|
#predelano do JEDNE TRANSAKCE
|
||||||
def delete_archived_runners_byIDs(ids: list[UUID]):
|
def delete_archived_runners_byIDs(ids: list[UUID]):
|
||||||
@ -1016,6 +1064,19 @@ def delete_archived_runners_byIDs(ids: list[UUID]):
|
|||||||
for id in ids:
|
for id in ids:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
print(str(id))
|
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)}';")
|
resh = c.execute(f"DELETE from runner_header WHERE runner_id='{str(id)}';")
|
||||||
print("header deleted",resh.rowcount)
|
print("header deleted",resh.rowcount)
|
||||||
resd = c.execute(f"DELETE from runner_detail WHERE runner_id='{str(id)}';")
|
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))
|
out.append(str(id) + ": " + str(resh.rowcount) + " " + str(resd.rowcount) + " " + str(resl.rowcount))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print("commit")
|
print("commit")
|
||||||
|
|
||||||
|
delete_report_files(id)
|
||||||
|
|
||||||
# if resh.rowcount == 0 or resd.rowcount == 0:
|
# if resh.rowcount == 0 or resd.rowcount == 0:
|
||||||
# return -1, "not found "+str(resh.rowcount) + " " + str(resd.rowcount) + " " + str(resl.rowcount)
|
# return -1, "not found "+str(resh.rowcount) + " " + str(resd.rowcount) + " " + str(resl.rowcount)
|
||||||
return 0, out
|
return 0, out
|
||||||
@ -1044,6 +1108,15 @@ def delete_archive_header_byID(id: UUID):
|
|||||||
res = execute_with_retry(c,statement)
|
res = execute_with_retry(c,statement)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print("deleted", res.rowcount)
|
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:
|
finally:
|
||||||
pool.release_connection(conn)
|
pool.release_connection(conn)
|
||||||
return res.rowcount
|
return res.rowcount
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from alpaca.data.enums import DataFeed
|
|||||||
from alpaca.data.historical import StockHistoricalDataClient
|
from alpaca.data.historical import StockHistoricalDataClient
|
||||||
from alpaca.data.requests import StockLatestQuoteRequest, StockBarsRequest, StockTradesRequest
|
from alpaca.data.requests import StockLatestQuoteRequest, StockBarsRequest, StockTradesRequest
|
||||||
from threading import Thread, current_thread
|
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 v2realbot.utils.tlog import tlog
|
||||||
from datetime import datetime, timedelta, date
|
from datetime import datetime, timedelta, date
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@ -21,6 +21,8 @@ import os
|
|||||||
from rich import print
|
from rich import print
|
||||||
import queue
|
import queue
|
||||||
from alpaca.trading.models import Calendar
|
from alpaca.trading.models import Calendar
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Trade offline data streamer, based on Alpaca historical data.
|
Trade offline data streamer, based on Alpaca historical data.
|
||||||
"""
|
"""
|
||||||
@ -212,7 +214,7 @@ class Trade_Offline_Streamer(Thread):
|
|||||||
cnt = 1
|
cnt = 1
|
||||||
|
|
||||||
|
|
||||||
for t in tradesResponse[symbol]:
|
for t in tqdm(tradesResponse[symbol]):
|
||||||
|
|
||||||
#protoze je zde cely den, poustime dal, jen ty relevantni
|
#protoze je zde cely den, poustime dal, jen ty relevantni
|
||||||
#pokud je start_time < trade < end_time
|
#pokud je start_time < trade < end_time
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import os,sys
|
import os,sys
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
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 alpaca.data.timeframe import TimeFrame, TimeFrameUnit
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
@ -13,7 +13,7 @@ import v2realbot.controller.services as cs
|
|||||||
from v2realbot.utils.ilog import get_log_window
|
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 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 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.staticfiles import StaticFiles
|
||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
from v2realbot.enums.enums import Env, Mode
|
from v2realbot.enums.enums import Env, Mode
|
||||||
@ -30,6 +30,9 @@ from v2realbot.utils.sysutils import get_environment
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from sqlite3 import OperationalError
|
from sqlite3 import OperationalError
|
||||||
from time import sleep
|
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
|
#from async io import Queue, QueueEmpty
|
||||||
|
|
||||||
# install()
|
# install()
|
||||||
@ -64,6 +67,7 @@ def api_key_auth(api_key: str = Depends(X_API_KEY)):
|
|||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
root = os.path.dirname(os.path.abspath(__file__))
|
root = os.path.dirname(os.path.abspath(__file__))
|
||||||
app.mount("/static", StaticFiles(html=True, directory=os.path.join(root, 'static')), name="static")
|
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")
|
#app.mount("/", StaticFiles(html=True, directory=os.path.join(root, 'static')), name="www")
|
||||||
|
|
||||||
security = HTTPBasic()
|
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}")
|
raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not changed: {res}:{runner_id}:{vals}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#edit archived runner ("note",..)
|
#edit archived runner ("note",..)
|
||||||
@app.patch("/archived_runners/{runner_id}", dependencies=[Depends(api_key_auth)])
|
@app.patch("/archived_runners/{runner_id}", dependencies=[Depends(api_key_auth)])
|
||||||
def _edit_archived_runners(archChange: RunArchiveChange, runner_id: UUID):
|
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:
|
else:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found {res} {set}")
|
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
|
#TestList APIS - do budoucna predelat SQL do separatnich funkci
|
||||||
@app.post('/testlists/', dependencies=[Depends(api_key_auth)])
|
@app.post('/testlists/', dependencies=[Depends(api_key_auth)])
|
||||||
def create_record(testlist: TestList):
|
def create_record(testlist: TestList):
|
||||||
|
|||||||
0
v2realbot/reporting/__init__.py
Normal file
0
v2realbot/reporting/__init__.py
Normal file
145
v2realbot/reporting/metricstools.py
Normal file
145
v2realbot/reporting/metricstools.py
Normal file
@ -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)
|
||||||
382
v2realbot/reporting/metricstoolsimage.py
Normal file
382
v2realbot/reporting/metricstoolsimage.py
Normal file
@ -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)
|
||||||
@ -279,7 +279,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="imagePreview" onclick="hideImage()">
|
||||||
|
<img src="" id="previewImg" style="width: auto; height: auto;" />
|
||||||
|
</div>
|
||||||
<div id="archive-table" class="flex-items">
|
<div id="archive-table" class="flex-items">
|
||||||
<label data-bs-toggle="collapse" data-bs-target="#archive-table-inner">
|
<label data-bs-toggle="collapse" data-bs-target="#archive-table-inner">
|
||||||
<h4>Past Runs</h4>
|
<h4>Past Runs</h4>
|
||||||
@ -299,6 +301,7 @@
|
|||||||
<button id="button_selpage" class="btn btn-outline-success btn-sm">Select all</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_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 id="button_export_csv" class="btn btn-outline-success btn-sm">Export csv</button>
|
||||||
|
<button id="button_report" class="btn btn-outline-success btn-sm">Report</button>
|
||||||
<!-- <button id="button_stopall" class="btn btn-outline-success btn-sm">Stop All</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> -->
|
<button id="button_refresh" class="btn btn-outline-success btn-sm">Refresh</button> -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -110,9 +110,61 @@ function prepare_export() {
|
|||||||
return trdList
|
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 () {
|
$(document).ready(function () {
|
||||||
archiveRecords.ajax.reload();
|
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
|
||||||
$('#button_export_xml').click(function () {
|
$('#button_export_xml').click(function () {
|
||||||
xmled = convertToXml(prepare_export())
|
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
|
//delete button
|
||||||
$('#button_delete_arch').click(function () {
|
$('#button_delete_arch').click(function () {
|
||||||
|
|||||||
@ -164,7 +164,14 @@ table.dataTable thead>tr>th.sorting_asc:before, table.dataTable thead>tr>th.sort
|
|||||||
--bs-gradient: none;
|
--bs-gradient: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#imagePreview {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
/* .btn-outline-success {
|
/* .btn-outline-success {
|
||||||
--bs-btn-color: #316164;
|
--bs-btn-color: #316164;
|
||||||
|
|||||||
@ -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)
|
#pokud jde o finalni FILL - pridame do pole tento celkovy relativnich profit (ze ktereho se pocita kumulativni relativni profit)
|
||||||
rel_profit_cum_calculated = 0
|
rel_profit_cum_calculated = 0
|
||||||
|
partial_exit = False
|
||||||
|
partial_last = False
|
||||||
if data.event == TradeEvent.FILL:
|
if data.event == TradeEvent.FILL:
|
||||||
#TODO pokud mame partial exit, tak se spravne vypocita relativni profit, ale
|
#jde o partial EXIT dvááme si rel.profit do docasne promenne, po poslednim exitu z nich vypocteme skutecny rel.profit
|
||||||
# je jen na mensi mnozszvi take z nej delat cum_calculate je blbost - OPRAVIT
|
if data.position_qty != 0:
|
||||||
self.state.rel_profit_cum.append(rel_profit)
|
self.state.docasny_rel_profit.append(rel_profit)
|
||||||
rel_profit_cum_calculated = round(np.sum(self.state.rel_profit_cum),5)
|
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
|
#zapsat profit do prescr.trades
|
||||||
for trade in self.state.vars.prescribedTrades:
|
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 = round((trade_profit / (vstup_cena * float(data.order.qty))) * 100,5)
|
||||||
|
|
||||||
rel_profit_cum_calculated = 0
|
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:
|
if data.event == TradeEvent.FILL:
|
||||||
self.state.rel_profit_cum.append(rel_profit)
|
#jde o partial EXIT dvááme si rel.profit do docasne promenne, po poslednim exitu z nich vypocteme skutecny rel.profit
|
||||||
rel_profit_cum_calculated = round(np.sum(self.state.rel_profit_cum),5)
|
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
|
#zapsat profit do prescr.trades
|
||||||
for trade in self.state.vars.prescribedTrades:
|
for trade in self.state.vars.prescribedTrades:
|
||||||
|
|||||||
@ -707,6 +707,7 @@ class StrategyState:
|
|||||||
self.runner_id = runner_id
|
self.runner_id = runner_id
|
||||||
self.bt = bt
|
self.bt = bt
|
||||||
self.dont_exit_already_activated = False
|
self.dont_exit_already_activated = False
|
||||||
|
self.docasny_rel_profit = []
|
||||||
self.ilog_save = ilog_save
|
self.ilog_save = ilog_save
|
||||||
self.sl_optimizer_short = optimsl.SLOptimizer(ptm.TradeDirection.SHORT)
|
self.sl_optimizer_short = optimsl.SLOptimizer(ptm.TradeDirection.SHORT)
|
||||||
self.sl_optimizer_long = optimsl.SLOptimizer(ptm.TradeDirection.LONG)
|
self.sl_optimizer_long = optimsl.SLOptimizer(ptm.TradeDirection.LONG)
|
||||||
|
|||||||
@ -82,7 +82,10 @@ def get_source_series(state, source: str):
|
|||||||
try:
|
try:
|
||||||
return state.bars[source]
|
return state.bars[source]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return state.indicators[source]
|
try:
|
||||||
|
return state.indicators[source]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
dict_name = source[:split_index]
|
dict_name = source[:split_index]
|
||||||
key = source[split_index + 1:]
|
key = source[split_index + 1:]
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from datetime import datetime
|
|||||||
from rich import print as printanyway
|
from rich import print as printanyway
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from v2realbot.strategyblocks.newtrade.conditions import go_conditions_met, common_go_preconditions_check
|
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):
|
def signal_search(state: StrategyState, data):
|
||||||
# SIGNAL sekce ve stratvars obsahuje signaly: Ty se skladaji z obecnych parametru a podsekce podminek.
|
# 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)
|
options = safe_get(state.vars.signals, name, None)
|
||||||
|
|
||||||
if options is 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
|
return
|
||||||
|
|
||||||
if common_go_preconditions_check(state, data, signalname=name, options=options) is False:
|
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:
|
if long_enabled is False:
|
||||||
state.ilog(lvl=1,e=f"{name} LONG DISABLED")
|
state.ilog(lvl=1,e=f"{name} LONG DISABLED")
|
||||||
if long_enabled and go_conditions_met(state, data,signalname=name, direction=TradeDirection.LONG):
|
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(
|
state.vars.prescribedTrades.append(Trade(
|
||||||
id=uuid4(),
|
id=uuid4(),
|
||||||
last_update=datetime.fromtimestamp(state.time).astimezone(zoneNY),
|
last_update=datetime.fromtimestamp(state.time).astimezone(zoneNY),
|
||||||
status=TradeStatus.READY,
|
status=TradeStatus.READY,
|
||||||
generated_by=name,
|
generated_by=name,
|
||||||
|
size=multiplier*state.vars.chunk,
|
||||||
|
size_multiplier = multiplier,
|
||||||
direction=TradeDirection.LONG,
|
direction=TradeDirection.LONG,
|
||||||
entry_price=None,
|
entry_price=None,
|
||||||
stoploss_value = None))
|
stoploss_value = None))
|
||||||
elif short_enabled and go_conditions_met(state, data, signalname=name, direction=TradeDirection.SHORT):
|
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(
|
state.vars.prescribedTrades.append(Trade(
|
||||||
id=uuid4(),
|
id=uuid4(),
|
||||||
last_update=datetime.fromtimestamp(state.time).astimezone(zoneNY),
|
last_update=datetime.fromtimestamp(state.time).astimezone(zoneNY),
|
||||||
status=TradeStatus.READY,
|
status=TradeStatus.READY,
|
||||||
generated_by=name,
|
generated_by=name,
|
||||||
|
size=multiplier*state.vars.chunk,
|
||||||
|
size_multiplier = multiplier,
|
||||||
direction=TradeDirection.SHORT,
|
direction=TradeDirection.SHORT,
|
||||||
entry_price=None,
|
entry_price=None,
|
||||||
stoploss_value = None))
|
stoploss_value = None))
|
||||||
|
|||||||
143
v2realbot/strategyblocks/newtrade/sizing.py
Normal file
143
v2realbot/strategyblocks/newtrade/sizing.py
Normal file
@ -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
|
||||||
29
v2realbot/tools/sizingpattervisual.py
Normal file
29
v2realbot/tools/sizingpattervisual.py
Normal file
@ -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()
|
||||||
@ -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()
|
rushtime = (datetime.combine(date.today(), business_hours["from"]) + timedelta(minutes=mins)).time()
|
||||||
return business_hours["from"] <= dt.time() < rushtime
|
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
|
#optimalized by BARD
|
||||||
def is_window_open(dt: datetime, start: int = 0, end: int = 390):
|
def is_window_open(dt: datetime, start: int = 0, end: int = 390):
|
||||||
""""
|
""""
|
||||||
|
|||||||
Reference in New Issue
Block a user