From 6cdc0a45c52666ab9437cfb03917c42aca230595 Mon Sep 17 00:00:00 2001 From: David Brazda Date: Wed, 6 Dec 2023 10:51:50 +0100 Subject: [PATCH] decomm ml, target algorithm a dalsi upravy --- requirements.txt | 68 ++- res_pred_act.png | Bin 0 -> 26264 bytes res_target.png | Bin 0 -> 20878 bytes run.sh | 15 + tested_runner.png | Bin 0 -> 21613 bytes testy/archive/interpolace.py | 12 +- v2realbot/ENTRY_ClassicSL_v01.py | 6 + v2realbot/LSTMevalrunner.py | 102 ----- v2realbot/LSTMtrain.py | 278 ------------- v2realbot/config.py | 1 + v2realbot/controller/services.py | 3 +- v2realbot/enums/enums.py | 1 + v2realbot/loader/aggregator.py | 201 ++++++++- v2realbot/main.py | 52 ++- v2realbot/ml/ml.py | 389 ------------------ v2realbot/ml/mlutils.py | 55 --- .../analyzer/WIP_daily_profit_distribution.py | 104 +++++ .../reporting/analyzer/find_optimal_cutoff.py | 41 +- .../analyzer/find_optimal_cutoff_REL.py | 244 +++++++++++ .../analyzer/summarize_trade_metrics.py | 129 ++++++ v2realbot/static/index.html | 26 +- v2realbot/static/js/common.js | 30 ++ v2realbot/static/js/ml.js | 33 ++ .../static/js/tables/archivetable/init.js | 13 +- v2realbot/static/main.css | 14 + v2realbot/strategy/base.py | 8 +- .../strategyblocks/activetrade/helpers.py | 2 +- v2realbot/strategyblocks/helpers.py | 2 +- .../strategyblocks/indicators/cbar_price.py | 2 +- .../indicators/custom/divergence.py | 42 ++ .../indicators/custom/target.py | 110 +++++ .../strategyblocks/inits/init_indicators.py | 6 +- 32 files changed, 1112 insertions(+), 877 deletions(-) create mode 100644 res_pred_act.png create mode 100644 res_target.png create mode 100644 tested_runner.png delete mode 100644 v2realbot/LSTMevalrunner.py delete mode 100644 v2realbot/LSTMtrain.py delete mode 100644 v2realbot/ml/ml.py delete mode 100644 v2realbot/ml/mlutils.py create mode 100644 v2realbot/reporting/analyzer/WIP_daily_profit_distribution.py create mode 100644 v2realbot/reporting/analyzer/find_optimal_cutoff_REL.py create mode 100644 v2realbot/reporting/analyzer/summarize_trade_metrics.py create mode 100644 v2realbot/static/js/common.js create mode 100644 v2realbot/static/js/ml.js create mode 100644 v2realbot/strategyblocks/indicators/custom/divergence.py create mode 100644 v2realbot/strategyblocks/indicators/custom/target.py diff --git a/requirements.txt b/requirements.txt index 1bf72ea..22fb813 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,12 @@ +absl-py==2.0.0 alpaca==1.0.0 alpaca-py==0.7.1 altair==4.2.2 anyio==3.6.2 appdirs==1.4.4 +appnope==0.1.3 asttokens==2.2.1 +astunparse==1.6.3 attrs==22.2.0 better-exceptions==0.3.3 bleach==6.0.0 @@ -13,6 +16,8 @@ certifi==2022.12.7 chardet==5.1.0 charset-normalizer==3.0.1 click==8.1.3 +colorama==0.4.6 +comm==0.1.4 contourpy==1.0.7 cycler==0.11.0 dash==2.9.1 @@ -20,35 +25,70 @@ dash-bootstrap-components==1.4.1 dash-core-components==2.0.0 dash-html-components==2.0.0 dash-table==5.0.0 +dateparser==1.1.8 decorator==5.1.1 +defusedxml==0.7.1 +dill==0.3.7 entrypoints==0.4 +exceptiongroup==1.1.3 executing==1.2.0 fastapi==0.95.0 Flask==2.2.3 +flatbuffers==23.5.26 fonttools==4.39.0 +fpdf2==2.7.6 +gast==0.4.0 gitdb==4.0.10 GitPython==3.1.31 +google-auth==2.23.0 +google-auth-oauthlib==1.0.0 +google-pasta==0.2.0 +grpcio==1.58.0 h11==0.14.0 +h5py==3.9.0 icecream==2.1.3 idna==3.4 +imageio==2.31.6 importlib-metadata==6.1.0 +ipython==8.17.2 +ipywidgets==8.1.1 itsdangerous==2.1.2 +jedi==0.19.1 +Jinja2==3.1.2 +joblib==1.3.2 jsonschema==4.17.3 +jupyterlab-widgets==3.0.9 +keras==2.13.1 kiwisolver==1.4.4 +libclang==16.0.6 +llvmlite==0.39.1 Markdown==3.4.3 markdown-it-py==2.2.0 MarkupSafe==2.1.2 +matplotlib==3.8.2 +matplotlib-inline==0.1.6 mdurl==0.1.2 +mlroom @ git+https://github.com/drew2323/mlroom.git@967b1e3b5071854910ea859eca68bf0c3e67f951 +mplfinance==0.12.10b0 msgpack==1.0.4 +mypy-extensions==1.0.0 newtulipy==0.4.6 -numpy==1.24.2 +numba==0.56.4 +numpy==1.23.5 +oauthlib==3.2.2 +opt-einsum==3.3.0 packaging==23.0 pandas==1.5.3 param==1.13.0 +parso==0.8.3 +pexpect==4.8.0 Pillow==9.4.0 plotly==5.13.1 +prompt-toolkit==3.0.39 proto-plus==1.22.2 protobuf==3.20.3 +ptyprocess==0.7.0 +pure-eval==0.2.2 pyarrow==11.0.0 pyasn1==0.4.8 pyasn1-modules==0.2.8 @@ -56,41 +96,65 @@ pyct==0.5.0 pydantic==1.10.5 pydeck==0.8.0 Pygments==2.14.0 +pyinstrument==4.5.3 Pympler==1.0.1 pyparsing==3.0.9 pyrsistent==0.19.3 pysos==1.3.0 python-dateutil==2.8.2 python-dotenv==1.0.0 +python-multipart==0.0.6 pytz==2022.7.1 pytz-deprecation-shim==0.1.0.post0 pyviz-comms==2.2.1 PyYAML==6.0 -requests==2.28.2 +regex==2023.10.3 +requests==2.31.0 +requests-oauthlib==1.3.1 rich==13.3.1 rsa==4.9 +schedule==1.2.1 +scikit-learn==1.3.1 +scipy==1.11.2 seaborn==0.12.2 semver==2.13.0 six==1.16.0 smmap==5.0.0 sniffio==1.3.0 sseclient-py==1.7.2 +stack-data==0.6.3 starlette==0.26.1 streamlit==1.20.0 structlog==23.1.0 +TA-Lib==0.4.28 tenacity==8.2.2 +tensorboard==2.13.0 +tensorboard-data-server==0.7.1 +tensorflow==2.13.0 +tensorflow-estimator==2.13.0 +tensorflow-io-gcs-filesystem==0.34.0 +termcolor==2.3.0 +threadpoolctl==3.2.0 +tinydb==4.7.1 +tinydb-serialization==2.1.0 +tinyflux==0.4.0 toml==0.10.2 tomli==2.0.1 toolz==0.12.0 tornado==6.2 tqdm==4.65.0 +traitlets==5.13.0 typing_extensions==4.5.0 tzdata==2023.2 tzlocal==4.3 urllib3==1.26.14 uvicorn==0.21.1 +-e git+https://github.com/drew2323/v2trading.git@d38bf0600fbadbffba78ae23625eaecd1febc7f4#egg=v2realbot validators==0.20.0 +wcwidth==0.2.9 webencodings==0.5.1 websockets==10.4 Werkzeug==2.2.3 +widgetsnbextension==4.0.9 +wrapt==1.15.0 zipp==3.15.0 diff --git a/res_pred_act.png b/res_pred_act.png new file mode 100644 index 0000000000000000000000000000000000000000..b77e9ea621bf7266fa395eb6d792bcd95f5027b3 GIT binary patch literal 26264 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 Lu6{1-oD!M<^bIqN literal 0 HcmV?d00001 diff --git a/run.sh b/run.sh index bcb9231..b4c1cf2 100755 --- a/run.sh +++ b/run.sh @@ -26,12 +26,27 @@ PYTHON_TO_USE="python3" #----END EDITABLE VARS------- +# Additions for handling strat.log backup +HISTORY_DIR="$HOME/stratlogs" +TIMESTAMP=$(date +"%Y%m%d-%H%M%S") +LOG_FILE="strat.log" +BACKUP_LOG_FILE="$HISTORY_DIR/${TIMESTAMP}_$LOG_FILE" + # If virtualenv specified & exists, using that version of python instead. if [ -d "$VIRTUAL_ENV_DIR" ]; then PYTHON_TO_USE="$VIRTUAL_ENV_DIR/bin/python" fi start() { + # Check and create history directory if it doesn't exist + [ ! -d "$HISTORY_DIR" ] && mkdir -p "$HISTORY_DIR" + + # Check if strat.log exists and back it up + if [ -f "$LOG_FILE" ]; then + mv "$LOG_FILE" "$BACKUP_LOG_FILE" + echo "Backed up log to $BACKUP_LOG_FILE" + fi + if [ ! -e "$OUTPUT_PID_PATH/$OUTPUT_PID_FILE" ]; then nohup "$PYTHON_TO_USE" ./$SCRIPT_TO_EXECUTE_PLUS_ARGS > strat.log 2>&1 & echo $! > "$OUTPUT_PID_PATH/$OUTPUT_PID_FILE" echo "Started $SCRIPT_TO_EXECUTE_PLUS_ARGS @ Process: $!" diff --git a/tested_runner.png b/tested_runner.png new file mode 100644 index 0000000000000000000000000000000000000000..2162db4420421d09b8699a32306b82afbb957bff GIT binary patch literal 21613 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 1: - #pro vis se cela tato sluzba volat v loopu - raise Exception("Vytvareni indikatoru dostupne zatim jen pro jeden runner") - -#scalujeme X -source_data = cfg.scalerX.fit_transform(source_data) - -#tady si vyzkousim i skrz vice runneru -X_eval, y_eval, y_eval_ref = cfg.create_sequences(combined_data=source_data, target_data=target_data,remove_cross_sequences=True, rows_in_day=rows_in_day) - -#toto nutne? -X_eval = np.array(X_eval) -y_eval = np.array(y_eval) -y_eval_ref = np.array(y_eval_ref) -#scaluji target - nemusis -#y_eval = cfg.scalerY.fit_transform(y_eval) - -X_eval = cfg.model.predict(X_eval) -X_eval = cfg.scalerY.inverse_transform(X_eval) -print("po predikci x_eval shape", X_eval.shape) - -#pokud mame dostupnou i target v runneru, pak pridame porovnavaci indikator -difference_mse = None -if len(y_eval) > 0: - #TODO porad to pliva 1 hodnotu - difference_mse = mean_squared_error(y_eval, X_eval,multioutput="raw_values") - -print("ted mam tedy dva nove sloupce") -print("X_eval", X_eval.shape) -if difference_mse is not None: - print("difference_mse", difference_mse.shape) -print(f"zplostime je, dopredu pridame {cfg.input_sequences-1} a dozadu nic") -#print(f"a melo by nam to celkem dat {len(bars['time'])}") -#tohle pak nejak doladit, ale vypada to good -#plus do druheho indikatoru pridat ten difference_mse - -#TODO jeste je posledni hodnota predikce nejak OFF (2.52... ) - podivat se na to -#TODO na produkci srovnat se skutecnym BT predictem (mozna zde bude treba seq-1) - -# prvni predikce nejspis uz bude na desítce -ind_pred = list(np.concatenate([np.zeros(cfg.input_sequences-1), X_eval.ravel()])) -print(ind_pred) -print(len(ind_pred)) -print("tada") -#ted k nim pridame - -if save_new_ind: - #novy ind ulozime do archrunnera (na produkci nejspis jen show) - res, sada = get_archived_runner_details_byID(runner_id) - if res == 0: - print("ok") - else: - print("error",res,sada) - raise Exception(f"error loading runner {runner_id} : {res} {sada}") - - sada["indicators"][0]["pred_added"] = ind_pred - - req, res = update_archive_detail(runner_id, sada) - print(f"indicator pred_added was ADDED to {runner_id}") - - -# Plot the predicted vs. actual -plt.plot(y_eval, label='Target') -plt.plot(X_eval, label='Predicted') -#TODO zde nejak vymyslet jinou pricelinu - jako lightweight chart -if difference_mse is not None: - plt.plot(difference_mse, label='diference') - plt.plot(y_eval_ref, label='reference column - vwap') -plt.plot() -plt.legend() -plt.show() diff --git a/v2realbot/LSTMtrain.py b/v2realbot/LSTMtrain.py deleted file mode 100644 index f3d6b2e..0000000 --- a/v2realbot/LSTMtrain.py +++ /dev/null @@ -1,278 +0,0 @@ -import numpy as np -from sklearn.preprocessing import StandardScaler -from sklearn.metrics import mean_squared_error -from sklearn.model_selection import train_test_split -import v2realbot.ml.mlutils as mu -from keras.layers import LSTM, Dense -import matplotlib.pyplot as plt -from v2realbot.ml.ml import ModelML -from v2realbot.enums.enums import PredOutput, Source, TargetTRFM -# from collections import defaultdict -# from operator import itemgetter -from joblib import load - -# region Notes - -#ZAKLAD PRO TRAINING SCRIPT na vytvareni model u -# TODO -# podpora pro BINARY TARGET -# podpora hyperpamaetru (activ.funkce sigmoid atp.) -# vyuzit distribuovane prostredi - nebo aspon vlastni VM -# dopracovat denni identifikatory typu lastday close, todays open atp. -# random SEARCH a grid search -# udelat nejaka model metadata (napr, trenovano na (runners+obdobi), nastaveni treningovych dat, počet epoch, hyperparametry, config atribu atp.) - mozna persistovat v db -# udelat nejake verzovani -# predelat do GUI a modulu -# vyuzit VectorBT na dohledani optimalizovanych parametru napr. pro buy,sell atp. Vyuzit podobne API na pripravu dat jako model. -# EVAL MODEL - umoznit vektorové přidání indikátoru do runneru (např. predikce v modulu, vectorBT, optimalizace atp) - vytvorit si na to API, podobne co mam, nacte runner, transformuje, sekvencuje, provede a pak zpetne transformuje a prida jako dalsi indikator. Lze pak použít i v gui. -# nove tlacitko "Display model prediction" na urovni archrunnera, které -# - má volbu model + jestli zobrazit jen predictionu jako novy indikator nebo i mse from ytarget (nutny i target) -# po spusteni pak: -# - zkonztoluje jestli runner ma indikatory,ktere odpovidaji features modelu (bar_ftrs, ind_ftrs, optional i target) -# - vektorově doplní predictionu (transformuje data, udela predictionu a Y transformuje zpet) -# - vysledek (jako nove indikatory) implantuje do runnerdetailu a zobrazi -# podivat se na dalsi parametry kerasu, napr. false positive atp. -# podivat se jeste na rozdil mezi vectorovou predikci a skalarni - proc je nekdy rozdil, odtrasovat - pripadne pogooglit -# odtrasovat, nekde je sum (zkusit si oboji v jednom skriptu a porovnat) - -#TODO NAPADY Na modely -#1.binary identifikace trendu napr. pokud nasledujici 3 bary rostou (0-1) nebo nasledujici bary roste momentum -#2.soustredit se na modely s vystupem 0-1 nebo -1 až 1 -#3.Vyzkouset jeden model, ktery by identifikoval trendy v obou smerech - -1 pro klesani a 1 pro stoupání. -#4.vyzkouset zda model vytvoreny z casti dne nebude funkcni na druhe casti (on the fly daily models) -#5.zkusit modely s a bez time (prizpusobit tomu kod v ModelML - zejmena jak na crossday sekvence) - mozna ze zecatku dat aspon pryc z indikatoru? -# Dat vsechny zbytecne features pryc, nechat tam jen ty podstatne - attention, tak cílím. -#6. zkusit vyuzit tickprice v nejaekm modelu, pripadne pak dalsi CBAR indikatory . vymslet tickbased features -#7. zkusit jako features nevyuzit standardni ceny, ale pouze indikatory reprezentujici chovani (fastslope,samebarslope,volume,tradencnt) -#8. relativni OHLC - model pouzivajici (jen) bary, ale misto hodnot ohlc udelat features reprezentujici vztahy(pomery) mezi temito velicinami. tzn. relativni ohlc -#9. jiny pristup by byl ucit model na konkretnich chunkach, ktere chci aby mi identifikoval. Např. určité úseky. Vymyslet. Buď nyni jako test intervaly, ale v budoucnu to treba jen nejak oznacit a poslat k nauceni. Pripadne pak udelat nejaky vycuc. -#10. mozna správným výběrem targetu, můžu taky naučit jen určité věci. Specializace. Stačí když se jednou dvakrát denně aktivuje. -# 11. udelat si go IN model, ktery pomuze strategii generovat vstup - staci jen aby mel trochu lepsi edge nez conditiony, o zbytek se postara logika strategie -# 12. model pro neagregované nebo jen filtroné či velmi lehce agregované trady? - tickprice -# 13. jako featury pouzit Fourierovo transformaci, na sekundovem baru nebo tickprice - -#DULEZITE -# soustredit se v modelech na predikci nasledujici hodnoty, ideálně nějaký vektor ukazující směr (např. 0 - 1, kde nula nebude růst, 1 - bude růst strmě) -# pro predikcí nějakého většího trendu, zkusti více modelů na různých rozlišení, každý ukazuje -# hodnotu na svém rozlišení a jeho kombinace mi může určit vstup. Zkusit zda by nešel i jeden model. -# Každopádně se soustředit -# 1) na další hodnotu (tzn. vstupy musí být bezprostředně ovlivňující tuto (samebasrlope, atp.)) -# 2) její výše ukazuje směr na tomto rozlišení -# 3) ideálně se učit z každého baru, tzn. cílová hodnota musí být známá u každého baru -# (binary ne, potřebuju linární vektor) - i když 1 a 0 target v závislosti na stoupání a klesání by mohla být ok, -# ale asi příliš restriktivní, spíš bych tam mohl dát jak moc. Tzn. +0.32, -0.04. Učilo by se to míru stoupání. -# Tu míru tam potřebuju zachovanou. -# pak si muzu rict, když je urcite pravdepodobnost, ze to bude stoupat (tzn. dalsi hodnota) na urovni 1,2,3 - tak jduvstup -# zkusit na nejnižší úrovni i předvídat CBARy, směr dalšího ticku. Vyzkoušet. - -##TODO - doma -#bar_features a ind_features do dokumentace SL classic, stejne tak conditional indikator a mathop indikator -#TODO - co je třeba vyvinout -# GENERATOR test intervalu (vstup name, note, od,do,step) -# napsat API, doma pak simple GUI -# vyuziti ATR (jako hranice historickeho rozsahu) - atr-up, atr-down -# nakreslit v grafu atru = close+atr, atrd = close-atr -# pripadne si vypocet atr nejak customizovat, prip. ruzne multiplikatory pro high low, pripadne si to vypocist podle sebe -# vyuziti: -# pro prekroceni nejake lajny, napr. ema nebo yesterdayclose -# - k identifikaci ze se pohybuje v jejim rozsahu -# - proste je to buffer, ktery musi byt prekonan, aby byla urcita akce -# pro learning pro vypocet conditional parametru (1,0,-1) prekroceni napr. dailyopen, yesterdayclose, gapclose -# kde 1 prekroceno, 0 v rozsahu (atr), -1 prekroceno dolu - to pomuze uceni -# vlastni supertrend strateige -# zaroven moznost vyuzit klouzave či parametrizovane atr, které se na základě -# určitých parametrů bude samo upravovat a cíleně vybočovat z KONTRA frekvencí, např. randomizovaný multiplier nebo nejak jinak ovlivneny minulým -# v indikatorech vsude kde je odkaz ma source jako hodnotu tak defaultne mit moznost uvest lookback, napr. bude treba porovnavat nejak cenu vs predposledni hodnotu ATRka (nechat az vyvstane pozadavek) -# zacit doma na ATRku si postavit supertrend, viz pinescript na ploše - - -#TODO - obecne vylepsovaky -# 1. v GUI graf container do n-TABů, mozna i draggable order, zaviratelne na Xko (innerContainer) -# 2. mit mozna specialni mod na pripravu dat (agreg+indikator, tzn. vse jen bez vstupů) - můžu pak zapracovat víc vectorové doplňování dat -# TOTO:: mozna by postacil vypnout backtester (tzn. no trades) - a projet jen indikatory. mozna by slo i vectorove optimalizovat. -# indikatory by se mohli predsunout pred next a next by se vubec nemusel volat (jen nekompatibilita s predch.strategiemi) -# 3. kombinace fastslope na fibonacci delkach (1,2,3,5..) jako dobry vstup pro ML -# 4. podivat se na attention based LSTM zda je v kerasu implementace -# do grafu přidat togglovatelné hranice barů určitých rozlišení - což mi jen udělá čáry Xs od sebe (dobré pro navrhování) -# 5. vymyslet optimalizovane vyuziti modelu na produkci (nejak mit zkompilovane, aby to bylo raketově pro skalár) - nyní to backtest zpomalí 4x -# 6. CONVNETS for time series forecasting - small 1D convnets can offer a fast alternative to RNNs for simple tasks such as text classification and timeseries forecasting. -# zkusit small conv1D pro identifikaci víření před trendem, např. jen 6 barů - identifikovat dobře target, musí jít o tutovku na targetu -# pro covnet zkusit cbar price, volume a time. Třeba to zachytí víření (ripples) -# Další oblasti k predikci jsou ripples, vlnky - předzvěst nějakého mocnějšího pohybu. A je pravda, že předtím se mohou objevit nějaké indicie. Ty zkus zachytit. -# Do runner_headers pridat bt_from, bt_to - pro razeni order_by, aby se runnery vzdy vraceli vzestupne dle data (pro machine l) - -#TODO -# vyvoj modelů workflow s LSTMtrain.py -# 1) POC - pouze zde ve skriptu, nad 1-2 runnery, okamžité zobrazení v plotu, -# optimalizace zakl. features a hyperparams. Zobrazit i u binary nejak cenu. -# 2) REALITY CHECK - trening modelu na batchi test intervalu, overeni ve strategii v BT, zobrazeni predikce v RT chartu -# 3) FINAL TRAINING -# testovani predikce - - -#TODO tady -# train model -# - train data- batch nebo runners -# - test data - batch or runners (s cim porovnavat/validovat) -# - vyber architektury -# - soucast skriptu muze byt i porovnavacka pripadne nejaky search optimalnich parametru - -#lstmtrain - podporit jednotlive kroky vyse -#modelML - udelat lepsi PODMINKY -#frontend? ma cenu? asi ano - GUI na model - new - train/retrain-change -# (vymyslet jak v gui chytře vybírat arch modelu a hyperparams, loss, optim - treba nejaka templata?) -# mozna ciselnik architektur s editačním polem pro kód -jen pár řádků(.add, .compile) přidat v editoru -# vymyslet jak to udělat pythonově -#testlist generator api - -# endregion - -#if null,the validation is made on 10% of train data -#runnery pro testovani -validation_runners = ["a38fc269-8df3-4374-9506-f0280d798854"] - -#u binary bude target bud hotovy indikator a nebo jej vytvorit on the fly -cfg = ModelML(name="model1", - version = "0.1", - note = None, - pred_output=PredOutput.LINEAR, - input_sequences = 10, - use_bars = True, - bar_features = ["volume","trades"], - ind_features = ["slope20", "ema20","emaFast","samebarslope","fastslope","fastslope4"], - target='target', #referencni hodnota pro target - napr pro graf - target_reference='vwap', - train_target_steps=3, - train_target_transformation=TargetTRFM.KEEPVAL, - train_runner_ids = ["08b7f96e-79bc-4849-9142-19d5b28775a8"], - train_batch_id = None, - train_epochs = 10, - train_remove_cross_sequences = True, - ) - -#TODO toto cele dat do TRAIN metody - vcetne pripadneho loopu a podpory API - -test_size = None - -#kdyz neplnime vstup, automaticky se loaduje training data z nastaveni classy -source_data, target_data, rows_in_day = cfg.load_data() - -if len(target_data) == 0: - raise Exception("target is empty - required for TRAINING - check target column name") - -np.set_printoptions(threshold=10,edgeitems=5) -#print("source_data", source_data) -#print("target_data", target_data) -print("rows_in_day", rows_in_day) -source_data = cfg.scalerX.fit_transform(source_data) - -#TODO mozna vyhodit to UNTR -#TODO asi vyhodit i target reference a vymyslet jinak - -#vytvořeni sekvenci po vstupních sadách (např. 10 barů) - výstup 3D např. #X_train (6205, 10, 14) -#doplneni transformace target data -X_train, y_train, y_train_ref = cfg.create_sequences(combined_data=source_data, - target_data=target_data, - remove_cross_sequences=cfg.train_remove_cross_sequences, - rows_in_day=rows_in_day) - -#zobrazime si transformovany target a jeho referncni sloupec -#ZHOMOGENIZOVAT OSY -plt.plot(y_train, label='Transf target') -plt.plot(y_train_ref, label='Ref target') -plt.plot() -plt.legend() -plt.show() - -print("After sequencing") -print("source:X_train", np.shape(X_train)) -print("target:y_train", np.shape(y_train)) -print("target:", y_train) -y_train = y_train.reshape(-1, 1) - -X_complete = np.array(X_train.copy()) -Y_complete = np.array(y_train.copy()) -X_train = np.array(X_train) -y_train = np.array(y_train) - -#target scaluji az po transformaci v create sequence -narozdil od X je stejny shape -y_train = cfg.scalerY.fit_transform(y_train) - - -if len(validation_runners) == 0: - test_size = 0.10 -# Split the data into training and test sets - kazdy vstupni pole rozdeli na dve -#nechame si takhle rozdelit i referencni sloupec -X_train, X_test, y_train, y_test, y_train_ref, y_test_ref = train_test_split(X_train, y_train, y_train_ref, test_size=test_size, shuffle=False) #random_state=42) - -print("Splittig the data") - -print("X_train", np.shape(X_train)) -print("X_test", np.shape(X_test)) -print("y_train", np.shape(y_train)) -print("y_test", np.shape(y_test)) -print("y_test_ref", np.shape(y_test_ref)) -print("y_train_ref", np.shape(y_train_ref)) - -#print(np.shape(X_train)) -# Define the input shape of the LSTM layer dynamically based on the reshaped X_train value -input_shape = (X_train.shape[1], X_train.shape[2]) - -# Build the LSTM model -#cfg.model = Sequential() -cfg.model.add(LSTM(128, input_shape=input_shape)) -cfg.model.add(Dense(1, activation="relu")) -#activation: Gelu, relu, elu, sigmoid... -# Compile the model -cfg.model.compile(loss='mse', optimizer='adam') -#loss: mse, binary_crossentropy - -# Train the model -cfg.model.fit(X_train, y_train, epochs=cfg.train_epochs) - -#save the model -cfg.save() - -#TBD db layer -cfg: ModelML = mu.load_model(cfg.name, cfg.version) - -# region Live predict -#EVALUATE SIM LIVE - PREDICT SCALAR - based on last X items -barslist, indicatorslist = cfg.load_runners_as_list(runner_id_list=["67b51211-d353-44d7-a58a-5ae298436da7"]) -#zmergujeme vsechny data dohromady -bars = mu.merge_dicts(barslist) -indicators = mu.merge_dicts(indicatorslist) -cfg.validate_available_features(bars, indicators) -#VSTUPEM JE standardni pole v strategii -value = cfg.predict(bars, indicators) -print("prediction for LIVE SIM:", value) -# endregion - -#EVALUATE TEST DATA - VECTOR BASED -#pokud mame eval runners pouzijeme ty, jinak bereme cast z testovacich dat -if len(validation_runners) > 0: - source_data, target_data, rows_in_day = cfg.load_data(runners_ids=validation_runners) - source_data = cfg.scalerX.fit_transform(source_data) - X_test, y_test, y_test_ref = cfg.create_sequences(combined_data=source_data, target_data=target_data,remove_cross_sequences=True, rows_in_day=rows_in_day) - -#prepnout ZDE pokud testovat cely bundle - jinak testujeme jen neznama -#X_test = X_complete -#y_test = Y_complete - -X_test = cfg.model.predict(X_test) -X_test = cfg.scalerY.inverse_transform(X_test) - -#target testovacim dat proc tu je reshape? y_test.reshape(-1, 1) -y_test = cfg.scalerY.inverse_transform(y_test) -#celkovy mean? nebo spis vector pro graf? -mse = mean_squared_error(y_test, X_test) -print('Test MSE:', mse) - -# Plot the predicted vs. actual -plt.plot(y_test, label='Actual') -plt.plot(X_test, label='Predicted') -#TODO zde nejak vymyslet jinou pricelinu - jako lightweight chart -plt.plot(y_test_ref, label='reference column - price') -plt.plot() -plt.legend() -plt.show() diff --git a/v2realbot/config.py b/v2realbot/config.py index 1905ee0..4a7a668 100644 --- a/v2realbot/config.py +++ b/v2realbot/config.py @@ -52,6 +52,7 @@ COUNT_API_REQUESTS = False #stratvars that cannot be changed in gui STRATVARS_UNCHANGEABLES = ['pendingbuys', 'blockbuy', 'jevylozeno', 'limitka'] DATA_DIR = user_data_dir("v2realbot") +MODEL_DIR = Path(DATA_DIR)/"models" #BT DELAYS #profiling PROFILING_NEXT_ENABLED = False diff --git a/v2realbot/controller/services.py b/v2realbot/controller/services.py index 76dfa9a..4c2d5eb 100644 --- a/v2realbot/controller/services.py +++ b/v2realbot/controller/services.py @@ -519,7 +519,8 @@ def batch_run_manager(id: UUID, runReq: RunRequest, rundays: list[RunDay]): print("Datum do", day.end) runReq.bt_from = day.start runReq.bt_to = day.end - runReq.note = f"{first_frm}-{last_frm} Batch {batch_id} #{cnt}/{cnt_max} {weekdayfilter_string} {day.name} N:{day.note} {note_from_run_request}" + #pozor z tohoto parsuje GUI Na batchheader + runReq.note = f"{first_frm}-{last_frm} Batch {batch_id} #{cnt}/{cnt_max} {weekdayfilter_string} {day.name} {day.note if day.note is not None else ''} N: {note_from_run_request}" #protoze jsme v ridicim vlaknu, poustime za sebou jednotlive stratiny v synchronnim modu res, id_val = run_stratin(id=id, runReq=runReq, synchronous=True, inter_batch_params=inter_batch_params) diff --git a/v2realbot/enums/enums.py b/v2realbot/enums/enums.py index d0f4095..9130c13 100644 --- a/v2realbot/enums/enums.py +++ b/v2realbot/enums/enums.py @@ -60,6 +60,7 @@ class RecordType(str, Enum): BAR = "bar" CBAR = "cbar" CBARVOLUME = "cbarvolume" + CBARDOLLAR = "cbardollar" CBARRENKO = "cbarrenko" TRADE = "trade" diff --git a/v2realbot/loader/aggregator.py b/v2realbot/loader/aggregator.py index 5d0abd6..216bd4c 100644 --- a/v2realbot/loader/aggregator.py +++ b/v2realbot/loader/aggregator.py @@ -178,14 +178,30 @@ class TradeAggregator: # return # else: pass - if self.rectype in (RecordType.BAR, RecordType.CBAR): - return await self.calculate_time_bar(data, symbol) + # if self.rectype in (RecordType.BAR, RecordType.CBAR): + # return await self.calculate_time_bar(data, symbol) - if self.rectype == RecordType.CBARVOLUME: - return await self.calculate_volume_bar(data, symbol) + # if self.rectype == RecordType.CBARVOLUME: + # return await self.calculate_volume_bar(data, symbol) - if self.rectype == RecordType.CBARRENKO: - return await self.calculate_renko_bar(data, symbol) + # if self.rectype == RecordType.CBARVOLUME: + # return await self.calculate_volume_bar(data, symbol) + + # if self.rectype == RecordType.CBARRENKO: + # return await self.calculate_renko_bar(data, symbol) + + match self.rectype: + case RecordType.BAR | RecordType.CBAR: + return await self.calculate_time_bar(data, symbol) + + case RecordType.CBARVOLUME: + return await self.calculate_volume_bar(data, symbol) + + case RecordType.CBARDOLLAR: + return await self.calculate_dollar_bar(data, symbol) + + case RecordType.CBARRENKO: + return await self.calculate_renko_bar(data, symbol) async def calculate_time_bar(self, data, symbol): #print("barstart",datetime.fromtimestamp(self.bar_start)) @@ -551,6 +567,179 @@ class TradeAggregator: else: return [] + #WIP - revidovant kod a otestovat + async def calculate_dollar_bar(self, data, symbol): + """" + Agreguje DOLLAR BARS - + hlavni promenne + - self.openedBar (dict) = stavová obsahují aktivní nepotvrzený bar + - confirmedBars (list) = nestavová obsahuje confirmnute bary, které budou na konci funkceflushnuty + """"" + #volume_bucket = 10000 #daily MA volume z emackova na 30 deleno 50ti - dat do configu + dollar_bucket = self.resolution + #potvrzene pripravene k vraceni + confirmedBars = [] + #potvrdi existujici a nastavi k vraceni + def confirm_existing(): + self.openedBar['confirmed'] = 1 + self.openedBar['vwap'] = self.vwaphelper / self.openedBar['volume'] + self.vwaphelper = 0 + + #ulozime zacatek potvrzeneho baru + #self.lastBarConfirmed = self.openedBar['time'] + + self.openedBar['updated'] = data['t'] + confirmedBars.append(deepcopy(self.openedBar)) + self.openedBar = None + #TBD po každém potvrzení zvýšíme čas o nanosekundu (pro zobrazení v gui) + #data['t'] = data['t'] + 0.000001 + + #init unconfirmed - velikost bucketu kontrolovana predtim + def initialize_unconfirmed(size): + #inicializuji pro nový bar + self.vwaphelper += (data['p'] * size) + self.barindex +=1 + self.openedBar = { + "close": data['p'], + "high": data['p'], + "low": data['p'], + "open": data['p'], + "volume": size, + "trades": 1, + "hlcc4": data['p'], + "confirmed": 0, + "time": datetime.fromtimestamp(data['t']), + "updated": data['t'], + "vwap": data['p'], + "index": self.barindex, + "resolution":dollar_bucket + } + + def update_unconfirmed(size): + #spočteme vwap - potřebujeme předchozí hodnoty + self.vwaphelper += (data['p'] * size) + self.openedBar['updated'] = data['t'] + self.openedBar['close'] = data['p'] + self.openedBar['high'] = max(self.openedBar['high'],data['p']) + self.openedBar['low'] = min(self.openedBar['low'],data['p']) + self.openedBar['volume'] = self.openedBar['volume'] + size + self.openedBar['trades'] = self.openedBar['trades'] + 1 + self.openedBar['vwap'] = self.vwaphelper / self.openedBar['volume'] + #pohrat si s timto round + self.openedBar['hlcc4'] = round((self.openedBar['high']+self.openedBar['low']+self.openedBar['close']+self.openedBar['close'])/4,3) + + #init new - confirmed + def initialize_confirmed(size): + #ulozime zacatek potvrzeneho baru + #self.lastBarConfirmed = datetime.fromtimestamp(data['t']) + self.barindex +=1 + confirmedBars.append({ + "close": data['p'], + "high": data['p'], + "low": data['p'], + "open": data['p'], + "volume": size, + "trades": 1, + "hlcc4":data['p'], + "confirmed": 1, + "time": datetime.fromtimestamp(data['t']), + "updated": data['t'], + "vwap": data['p'], + "index": self.barindex, + "resolution": dollar_bucket + }) + + #current trade dollar value + trade_dollar_val = int(data['s'])*float(data['p']) + + #existuje stávající bar a vejdeme se do nej + if self.openedBar is not None and trade_dollar_val + self.openedBar['volume']*self.openedBar['close'] < dollar_bucket: + #vejdeme se do stávajícího baru (tzn. neprekracujeme bucket) + update_unconfirmed(int(data['s'])) + #updatujeme stávající nepotvrzeny bar + #nevejdem se do nej nebo neexistuje predchozi bar + else: + #1)existuje predchozi bar - doplnime zbytkem do valikosti bucketu a nastavime confirmed + if self.openedBar is not None: + + #doplnime je zbytkem (v bucket left-je zbyvajici volume) + opened_bar_dollar_val = self.openedBar['volume']*self.openedBar['close'] + bucket_left = int((dollar_bucket - opened_bar_dollar_val)/float(data['p'])) + # - update and confirm bar + update_unconfirmed(bucket_left) + confirm_existing() + + #zbytek mnozství jde do dalsiho zpracovani + data['s'] = int(data['s']) - bucket_left + #nastavime cas o nanosekundu vyssi + data['t'] = round((data['t']) + 0.000001,6) + + #2 vytvarime novy bar (bary) a vejdeme se do nej + if int(data['s'])*float(data['p']) < dollar_bucket: + #vytvarime novy nepotvrzeny bar + initialize_unconfirmed(int(data['s'])) + #nevejdeme se do nej - pak vytvarime 1 až N dalsich baru (posledni nepotvrzený) + else: + # >>> for i in range(0, 550, 500): + # ... print(i) + # ... + # 0 + # 500 + + #vytvarime plne potvrzene buckety (kolik se jich plne vejde) + for size in range(int(dollar_bucket/float(data['p'])), int(data['s']), int(dollar_bucket/float(data['p']))): + initialize_confirmed(dollar_bucket/float(data['p'])) + #nastavime cas o nanosekundu vyssi + data['t'] = round((data['t']) + 0.000001,6) + #create complete full bucket with same prices and size + #naplnit do return pole + + #pokud je zbytek vytvorime z nej nepotvrzeny bar + zbytek = int(data['s'])*float(data['p']) % dollar_bucket + + #ze zbytku vytvorime nepotvrzeny bar + if zbytek > 0: + #prevedeme zpatky na volume + zbytek = int(zbytek/float(data['p'])) + initialize_unconfirmed(zbytek) + #create new open bar with size zbytek s otevrenym + + #je cena stejna od predchoziho tradu? pro nepotvrzeny cbar vracime jen pri zmene ceny + if self.last_price == data['p']: + self.diff_price = False + else: + self.diff_price = True + self.last_price = data['p'] + + if float(data['t']) - float(self.lasttimestamp) < GROUP_TRADES_WITH_TIMESTAMP_LESS_THAN: + self.trades_too_close = True + else: + self.trades_too_close = False + + #uložíme do předchozí hodnoty (poznáme tak open a close) + self.lasttimestamp = data['t'] + self.iterace += 1 + # print(self.iterace, data) + + #pokud mame confirm bary, tak FLUSHNEME confirm a i případný open (zrejme se pak nejaky vytvoril) + if len(confirmedBars) > 0: + return_set = confirmedBars + ([self.openedBar] if self.openedBar is not None else []) + confirmedBars = [] + return return_set + + #nemame confirm, FLUSHUJEME CBARVOLUME open - neresime zmenu ceny, ale neposilame kulomet (pokud nam nevytvari conf. bar) + if self.openedBar is not None and self.rectype == RecordType.CBARDOLLAR: + + #zkousime pustit i stejnou cenu(potrebujeme kvuli MYSELLU), ale blokoval kulomet,tzn. trady mensi nez GROUP_TRADES_WITH_TIMESTAMP_LESS_THAN (1ms) + #if self.diff_price is True: + if self.trades_too_close is False: + return [self.openedBar] + else: + return [] + else: + return [] + + async def calculate_renko_bar(self, data, symbol): """" Agreguje RENKO BARS - dle brick size diff --git a/v2realbot/main.py b/v2realbot/main.py index 8f79e37..dd8f0ba 100644 --- a/v2realbot/main.py +++ b/v2realbot/main.py @@ -1,11 +1,11 @@ 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, MEDIA_DIRECTORY, LOG_FILE +from v2realbot.config import WEB_API_KEY, DATA_DIR, MEDIA_DIRECTORY, LOG_FILE, MODEL_DIR from alpaca.data.timeframe import TimeFrame, TimeFrameUnit from datetime import datetime import os from rich import print -from fastapi import FastAPI, Depends, HTTPException, status +from fastapi import FastAPI, Depends, HTTPException, status, File, UploadFile from fastapi.security import APIKeyHeader import uvicorn from uuid import UUID @@ -455,6 +455,16 @@ def _delete_archived_runners_byIDs(runner_ids: list[UUID]): elif res < 0: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error: {res}:{id}") +#get runners list based on batch_id +@app.get("/archived_runners/batch/{batch_id}", dependencies=[Depends(api_key_auth)]) +def _get_archived_runnerslist_byBatchID(batch_id: str) -> list[UUID]: + res, set =cs.get_archived_runnerslist_byBatchID(batch_id) + if res == 0: + return set + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found") + + #delete archive runner from header and detail @app.delete("/archived_runners/batch/{batch_id}", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK) def _delete_archived_runners_byBatchID(batch_id: str): @@ -763,6 +773,44 @@ def delete_item(item_id: int) -> dict: # endregion +#model section +#UPLOAD MODEL +@app.post("/model/upload_model", dependencies=[Depends(api_key_auth)]) +async def upload_model(file: UploadFile = File(...)): + # Specify the directory to save the file + #save_directory = DATA_DIR+'/models/' + save_directory = MODEL_DIR + + os.makedirs(save_directory, exist_ok=True) + + # Extract just the filename, discarding any path information + base_filename = os.path.basename(file.filename) + file_path = os.path.join(save_directory, base_filename) + + # Save the uploaded file + with open(file_path, "wb") as buffer: + while True: + data = await file.read(1024) # Read in chunks + if not data: + break + buffer.write(data) + + print(f"saved to {file_path=} file:{base_filename=}") + + return {"filename": base_filename, "location": file_path} + +#LIST MODELS +@app.get("/model/list-models", dependencies=[Depends(api_key_auth)]) +def list_models(): + #models_directory = DATA_DIR + '/models/' + models_directory = MODEL_DIR + # Ensure the directory exists + if not os.path.exists(models_directory): + return {"error": "Models directory does not exist."} + + # List all files in the directory + model_files = os.listdir(models_directory) + return {"models": model_files} # Thread function to insert data from the queue into the database def insert_queue2db(): diff --git a/v2realbot/ml/ml.py b/v2realbot/ml/ml.py deleted file mode 100644 index 39cfa93..0000000 --- a/v2realbot/ml/ml.py +++ /dev/null @@ -1,389 +0,0 @@ -# from sklearn.preprocessing import StandardScaler -# # from keras.models import Sequential -# from v2realbot.enums.enums import PredOutput, Source, TargetTRFM -# from v2realbot.config import DATA_DIR -# from joblib import dump -# # import v2realbot.ml.mlutils as mu -# from v2realbot.utils.utils import slice_dict_lists -# import numpy as np -# from copy import deepcopy -# import v2realbot.controller.services as cs -# #Basic classes for machine learning -# #drzi model a jeho zakladni nastaveni - -# #Sample Data -# sample_bars = { -# 'time': [1, 2, 3, 4, 5,6,7,8,9,10,11,12,13,14,15], -# 'high': [10, 11, 12, 13, 14,10, 11, 12, 13, 14,10, 11, 12, 13, 14], -# 'low': [8, 9, 7, 6, 8,8, 9, 7, 6, 8,8, 9, 7, 6, 8], -# 'volume': [1000, 1200, 900, 1100, 1300,1000, 1200, 900, 1100, 1300,1000, 1200, 900, 1100, 1300], -# 'close': [9, 10, 11, 12, 13,9, 10, 11, 12, 13,9, 10, 11, 12, 13], -# 'open': [9, 10, 8, 8, 8,9, 10, 8, 8, 8,9, 10, 8, 8, 8], -# 'resolution': [1, 1, 1, 1, 1,1, 1, 1, 1, 1,1, 1, 1, 1, 1] -# } - -# sample_indicators = { -# 'time': [1, 2, 3, 4, 5,6,7,8,9,10,11,12,13,14,15], -# 'fastslope': [90, 95, 100, 110, 115,90, 95, 100, 110, 115,90, 95, 100, 110, 115], -# 'fsdelta': [90, 95, 100, 110, 115,90, 95, 100, 110, 115,90, 95, 100, 110, 115], -# 'fastslope2': [90, 95, 100, 110, 115,90, 95, 100, 110, 115,90, 95, 100, 110, 115], -# 'ema': [1000, 1200, 900, 1100, 1300,1000, 1200, 900, 1100, 1300,1000, 1200, 900, 1100, 1300] -# } - -# #Trida, která drzi instanci ML modelu a jeho konfigurace -# #take se pouziva jako nastroj na pripravu dat pro train a predikci -# #pozor samotna data trida neobsahuje, jen konfiguraci a pak samotny model -# class ModelML: -# def __init__(self, name: str, -# pred_output: PredOutput, -# bar_features: list, -# ind_features: list, -# input_sequences: int, -# target: str, -# target_reference: str, -# train_target_steps: int, #train -# train_target_transformation: TargetTRFM, #train -# train_epochs: int, #train -# train_runner_ids: list = None, #train -# train_batch_id: str = None, #train -# version: str = "1", -# note : str = None, -# use_bars: bool = True, -# train_remove_cross_sequences: bool = False, #train -# #standardne StandardScaler -# scalerX: StandardScaler = StandardScaler(), -# scalerY: StandardScaler = StandardScaler(), -# model, #Sequential = Sequential() -# )-> None: - -# self.name = name -# self.version = version -# self.note = note -# self.pred_output: PredOutput = pred_output -# #model muze byt take bez barů, tzn. jen indikatory -# self.use_bars = use_bars -# #zajistime poradi -# bar_features.sort() -# ind_features.sort() -# self.bar_features = bar_features -# self.ind_features = ind_features -# if (train_runner_ids is None or len(train_runner_ids) == 0) and train_batch_id is None: -# raise Exception("train_runner_ids nebo train_batch_id musi byt vyplnene") -# self.train_runner_ids = train_runner_ids -# self.train_batch_id = train_batch_id -# #target cílový sloupec, který je používám přímo nebo transformován na binary -# self.target = target -# self.target_reference = target_reference -# self.train_target_steps = train_target_steps -# self.train_target_transformation = train_target_transformation -# self.input_sequences = input_sequences -# self.train_epochs = train_epochs -# #keep cross sequences between runners -# self.train_remove_cross_sequences = train_remove_cross_sequences -# self.scalerX = scalerX -# self.scalerY = scalerY -# self.model = model - -# def save(self): -# filename = mu.get_full_filename(self.name,self.version) -# dump(self, filename) -# print(f"model {self.name} save") - -# #create X data with features -# def column_stack_source(self, bars, indicators, verbose = 1) -> np.array: -# #create SOURCE DATA with features -# # bars and indicators dictionary and features as input -# poradi_sloupcu_inds = [feature for feature in self.ind_features if feature in indicators] -# indicator_data = np.column_stack([indicators[feature] for feature in self.ind_features if feature in indicators]) - -# if len(bars)>0: -# bar_data = np.column_stack([bars[feature] for feature in self.bar_features if feature in bars]) -# poradi_sloupcu_bars = [feature for feature in self.bar_features if feature in bars] -# if verbose == 1: -# print("poradi sloupce v source_data", str(poradi_sloupcu_bars + poradi_sloupcu_inds)) -# combined_day_data = np.column_stack([bar_data,indicator_data]) -# else: -# combined_day_data = indicator_data -# if verbose == 1: -# print("poradi sloupce v source_data", str(poradi_sloupcu_inds)) -# return combined_day_data - -# #create TARGET(Y) data -# def column_stack_target(self, bars, indicators) -> np.array: -# target_base = [] -# target_reference = [] -# try: -# try: -# target_base = bars[self.target] -# except KeyError: -# target_base = indicators[self.target] -# try: -# target_reference = bars[self.target_reference] -# except KeyError: -# target_reference = indicators[self.target_reference] -# except KeyError: -# pass -# target_day_data = np.column_stack([target_base, target_reference]) -# return target_day_data - -# def load_runners_as_list(self, runner_id_list = None, batch_id = None): -# """Loads all runners data (bars, indicators) for given runners into list of dicts. - -# List of runners/train_batch_id may be provided, or self.train_runner_ids/train_batch_id is taken instead. - -# Returns: -# tuple (barslist, indicatorslist,) - lists with dictionaries for each runner -# """ -# if runner_id_list is not None: -# runner_ids = runner_id_list -# print("loading runners for ",str(runner_id_list)) -# elif batch_id is not None: -# print("Loading runners for train_batch_id:", batch_id) -# res, runner_ids = cs.get_archived_runnerslist_byBatchID(batch_id) -# elif self.train_batch_id is not None: -# print("Loading runners for TRAINING BATCH self.train_batch_id:", self.train_batch_id) -# res, runner_ids = cs.get_archived_runnerslist_byBatchID(self.train_batch_id) -# #pripadne bereme z listu runneru -# else: -# runner_ids = self.train_runner_ids -# print("loading runners for TRAINING runners ",str(self.train_runner_ids)) - - -# barslist = [] -# indicatorslist = [] -# ind_keys = None -# for runner_id in runner_ids: -# bars, indicators = mu.load_runner(runner_id) -# print(f"runner:{runner_id}") -# if self.use_bars: -# barslist.append(bars) -# print(f"bars keys {len(bars)} lng {len(bars[self.bar_features[0]])}") -# indicatorslist.append(indicators) -# print(f"indi keys {len(indicators)} lng {len(indicators[self.ind_features[0]])}") -# if ind_keys is not None and ind_keys != len(indicators): -# raise Exception("V runnerech musi byt stejny pocet indikatoru") -# else: -# ind_keys = len(indicators) - -# return barslist, indicatorslist - -# #toto nejspis rozdelit na TRAIN mod (kdy ma smysl si brat nataveni napr. remove cross) -# def create_sequences(self, combined_data, target_data = None, remove_cross_sequences: bool = False, rows_in_day = None): -# """Creates sequences of given length seq and optionally target N steps in the future. - -# Returns X(source) a Y(transformed target) - vrací take Y_untransformed - napr. referencni target column pro zobrazeni v grafu (napr. cenu) - -# Volby pro transformaci targetu: -# - KEEPVAL (keep value as is) -# - KEEPVAL_MOVE(keep value, move target N steps in the future) - -# další na zámysl (nejspíš ale data budu připravovat ve stratu a využívat jen KEEPy nahoře) -# - BINARY_prefix - sloupec založený na podmínce, výsledek je 0,1 -# - BINARY_TREND RISING - podmínka založena, že v target columnu stoupají/klesají po target N steps -# (podvarianty BINARY TREND RISING(0-1), FALLING(0-1), BOTH(-1 - )) -# - BINARY_READY - předpřipravený sloupec(vytvořený ve strategii jako indikator), stačí jen posunout o target step -# - BINARY_READY_POSUNUTY - předpřipraveny sloupec (již posunutýo o target M) - stačí brát as is - -# Args: -# combined_data: A list of combined data. -# target_data: A list of target data (0-target,1-target ref.column) -# remove_cross_sequences: If to remove crossday sequences -# rows_in_day: helper dict to remove crossday sequences -# return_untr: whether to return untransformed reference column - -# Returns: -# A list of X sequences and a list of y sequences. -# """ - -# if remove_cross_sequences is True and rows_in_day is None: -# raise Exception("To remove crossday sequences, rows_in_day param required.") - -# if target_data is not None and len(target_data) > 0: -# target_data_untr = target_data[:,1] -# target_data = target_data[:,0] -# else: -# target_data_untr = [] -# target_data = [] - -# X_train = [] -# y_train = [] -# y_untr = [] -# #comb data shape (4073, 13) -# #target shape (4073, 1) -# print("Start Sequencing") -# #range sekvence podle toho jestli je pozadovan MOVE nebo NE -# if self.train_target_transformation == TargetTRFM.KEEPVAL_MOVE: -# right_offset = self.input_sequences + self.train_target_steps -# else: -# right_offset= self.input_sequences -# for i in range(len(combined_data) - right_offset): - -# #take neresime cross sekvence kdyz neni vyplneni target nebo neni vyplnena rowsinaday -# if remove_cross_sequences is True and not self.is_same_day(i,i + right_offset, rows_in_day): -# print(f"sekvence vyrazena. NEW Zacatek {combined_data[i, 0]} konec {combined_data[i + right_offset, 0]}") -# continue - -# #pridame sekvenci -# X_train.append(combined_data[i:i + self.input_sequences]) - -# #target hodnotu bude ponecha (na radku mame jiz cilovy target) -# #nebo vezme hodnotu z N(train_target_steps) baru vpredu a da jako target k radku -# #je rizeno nastavenim right_offset vyse -# if target_data is not None and len(target_data) > 0: -# y_train.append(target_data[i + right_offset]) - -# #udela binary transformaci targetu -# # elif self.target_transformation == TargetTRFM.BINARY_TREND_UP: -# # #mini loop od 0 do počtu target steps - zda jsou successively rising -# # #radeji budu resit vizualne conditional indikatorem pri priprave dat -# # rising = False -# # for step in range(0,self.train_target_steps): -# # if target_data[i + self.input_sequences + step] < target_data[i + self.input_sequences + step + 1]: -# # rising = True -# # else: -# # rising = False -# # break -# # y_train.append([1] if rising else [0]) -# # #tato zakomentovana varianta porovnava jen cenu ted a cenu na target baru -# # #y_train.append([1] if target_data[i + self.input_sequences] < target_data[i + self.input_sequences + self.train_target_steps] else [0]) -# if target_data is not None and len(target_data) > 0: -# y_untr.append(target_data_untr[i + self.input_sequences]) -# return np.array(X_train), np.array(y_train), np.array(y_untr) - -# def is_same_day(self, idx_start, idx_end, rows_in_day): -# """Helper for sequencing enables to recognize if the start/end index are from the same day. - -# Used for sequences to remove cross runner(day) sequences. - -# Args: -# idx_start: Start index -# idx_end: End index -# rows_in_day: 1D array containing number of rows(bars,inds) for each day. -# Cumsumed defines edges where each day ends. [10,30,60] - -# Returns: -# A boolean - -# refactor to vectors if possible -# i_b, i_e -# podm_pole = i_b= pole -# [10,30,60] -# """ -# for i in rows_in_day: -# #jde o polozku na pomezi - vyhazujeme -# if idx_start < i and idx_end >= i: -# return False -# if idx_start < i and idx_end < i: -# return True -# return None - -# #vytvori X a Y data z nastaveni self -# #pro vybrane runnery stahne data, vybere sloupce dle faature a target -# #a vrátí jako sloupce v numpy poli -# #zaroven vraci i rows_in_day pro nasledny sekvencing -# def load_data(self, runners_ids: list = None, batch_id: list = None, source: Source = Source.RUNNERS): -# """Service to load data for the model. Can be used for training or for vector prediction. - -# If input data are not provided, it will get the value from training model configuration (train_runners_ids, train_batch_id) - -# Args: -# runner_ids: -# batch_id: -# source: To load sample data. - -# Returns: -# source_data,target_data,rows_in_day -# """ -# rows_in_day = [] -# indicatorslist = [] -# #bud natahneme samply -# if source == Source.SAMPLES: -# if self.use_bars: -# bars = sample_bars -# else: -# bars = {} -# indicators = sample_indicators -# indicatorslist.append(indicators) -# #nebo dotahneme pozadovane runnery -# else: -# #nalodujeme vsechny runnery jako listy (bud z runnerids nebo dle batchid) -# barslist, indicatorslist = self.load_runners_as_list(runner_id_list=runners_ids, batch_id=batch_id) -# #nerozumim -# bl = deepcopy(barslist) -# il = deepcopy(indicatorslist) -# #a zmergujeme jejich data dohromady -# bars = mu.merge_dicts(bl) -# indicators = mu.merge_dicts(il) - -# #zaroven vytvarime pomocny list, kde stale drzime pocet radku per day (pro nasledny sekvencing) -# #zatim nad indikatory - v budoucnu zvazit, kdyby jelo neco jen nad barama -# for i, val in enumerate(indicatorslist): -# #pro prvni klic z indikatoru pocteme cnt -# pocet = len(indicatorslist[i][self.ind_features[0]]) -# print("pro runner vkladame pocet", pocet) -# rows_in_day.append(pocet) - -# rows_in_day = np.array(rows_in_day) -# rows_in_day = np.cumsum(rows_in_day) -# print("celkove pole rows_in_day(cumsum):", rows_in_day) - -# print("Data LOADED.") -# print(f"number of indicators {len(indicators)}") -# print(f"number of bar elements{len(bars)}") -# print(f"ind list length {len(indicators['time'])}") -# print(f"bar list length {len(bars['time'])}") - -# self.validate_available_features(bars, indicators) - -# print("Preparing FEATURES") -# source_data, target_data = self.stack_bars_indicators(bars, indicators) -# return source_data, target_data, rows_in_day - -# def validate_available_features(self, bars, indicators): -# for k in self.bar_features: -# if not k in bars.keys(): -# raise Exception(f"Missing bar feature {k}") - -# for k in self.ind_features: -# if not k in indicators.keys(): -# raise Exception(f"Missing ind feature {k}") - -# def stack_bars_indicators(self, bars, indicators): -# print("Stacking dicts to numpy") -# print("Source - X") -# source_data = self.column_stack_source(bars, indicators) -# print("shape", np.shape(source_data)) -# print("Target - Y", self.target) -# target_data = self.column_stack_target(bars, indicators) -# print("shape", np.shape(target_data)) - -# return source_data, target_data - -# #pomocna sluzba, ktera provede vsechny transformace a inverzni scaling a vyleze z nej predikce -# #vstupem je standardni format ve strategii (state.bars, state.indicators) -# #vystupem je jedna hodnota -# def predict(self, bars, indicators) -> float: -# #oriznuti podle seqence - pokud je nastaveno v modelu -# lastNbars = slice_dict_lists(bars, self.input_sequences) -# lastNindicators = slice_dict_lists(indicators, self.input_sequences) -# # print("last5bars", lastNbars) -# # print("last5indicators",lastNindicators) - -# combined_live_data = self.column_stack_source(lastNbars, lastNindicators, verbose=0) -# #print("combined_live_data",combined_live_data) -# combined_live_data = self.scalerX.transform(combined_live_data) -# combined_live_data = np.array(combined_live_data) -# #print("last 5 values combined data shape", np.shape(combined_live_data)) - -# #converts to 3D array -# # 1 number of samples in the array. -# # 2 represents the sequence length. -# # 3 represents the number of features in the data. -# combined_live_data = combined_live_data.reshape((1, self.input_sequences, combined_live_data.shape[1])) - -# # Make a prediction -# prediction = self.model(combined_live_data, training=False) -# #prediction = prediction.reshape((1, 1)) -# # Convert the prediction back to the original scale -# prediction = self.scalerY.inverse_transform(prediction) -# return float(prediction) diff --git a/v2realbot/ml/mlutils.py b/v2realbot/ml/mlutils.py deleted file mode 100644 index 5207e69..0000000 --- a/v2realbot/ml/mlutils.py +++ /dev/null @@ -1,55 +0,0 @@ -import numpy as np -# import v2realbot.controller.services as cs -from joblib import load -from v2realbot.config import DATA_DIR - -def get_full_filename(name, version = "1"): - return DATA_DIR+'/models/'+name+'_v'+version+'.pkl' - -def load_model(name, version = "1"): - filename = get_full_filename(name, version) - return load(filename) - -#pomocne funkce na manipulaci s daty - -def merge_dicts(dict_list): - # Initialize an empty merged dictionary - merged_dict = {} - - # Iterate through the dictionaries in the list - for i,d in enumerate(dict_list): - for key, value in d.items(): - if key in merged_dict: - merged_dict[key] += value - else: - merged_dict[key] = value - #vlozime element s idenitfikaci runnera - - return merged_dict - - # # Initialize the merged dictionary with the first dictionary in the list - # merged_dict = dict_list[0].copy() - # merged_dict["index"] = [] - - # # Iterate through the remaining dictionaries and concatenate their lists - # for i, d in enumerate(dict_list[1:]): - # merged_dict["index"] = - # for key, value in d.items(): - # if key in merged_dict: - # merged_dict[key] += value - # else: - # merged_dict[key] = value - - # return merged_dict - -def load_runner(runner_id): - res, sada = cs.get_archived_runner_details_byID(runner_id) - if res == 0: - print("ok") - else: - print("error",res,sada) - raise Exception(f"error loading runner {runner_id} : {res} {sada}") - - bars = sada["bars"] - indicators = sada["indicators"][0] - return bars, indicators diff --git a/v2realbot/reporting/analyzer/WIP_daily_profit_distribution.py b/v2realbot/reporting/analyzer/WIP_daily_profit_distribution.py new file mode 100644 index 0000000..4794a93 --- /dev/null +++ b/v2realbot/reporting/analyzer/WIP_daily_profit_distribution.py @@ -0,0 +1,104 @@ +import matplotlib +import matplotlib.dates as mdates +matplotlib.use('Agg') # Set the Matplotlib backend to 'Agg' +import matplotlib.pyplot as plt +from matplotlib.ticker import MaxNLocator +import seaborn as sns +import pandas as pd +from datetime import datetime +from typing import List +from enum import Enum +import numpy as np +import v2realbot.controller.services as cs +from rich import print +from v2realbot.common.model import AnalyzerInputs +from v2realbot.common.PrescribedTradeModel import TradeDirection, TradeStatus, Trade, TradeStoplossType +from v2realbot.utils.utils import isrising, isfalling,zoneNY, price2dec, safe_get#, print +from pathlib import Path +from v2realbot.config import WEB_API_KEY, DATA_DIR, MEDIA_DIRECTORY +from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account, OrderSide +from io import BytesIO +from v2realbot.utils.historicals import get_historical_bars +from alpaca.data.timeframe import TimeFrame, TimeFrameUnit +from collections import defaultdict +from scipy.stats import zscore +from io import BytesIO +from v2realbot.reporting.load_trades import load_trades +from typing import Tuple, Optional, List +from traceback import format_exc +import pandas as pd + +def daily_profit_distribution(runner_ids: list = None, batch_id: str = None, stream: bool = False): + try: + res, trades, days_cnt = load_trades(runner_ids, batch_id) + if res != 0: + raise Exception("Error in loading trades") + + #print(trades) + + # Convert list of Trade objects to DataFrame + trades_df = pd.DataFrame([t.__dict__ for t in trades if t.status == "closed"]) + + # Ensure 'exit_time' is a datetime object and make it timezone-naive if necessary + trades_df['exit_time'] = pd.to_datetime(trades_df['exit_time']).dt.tz_convert(zoneNY) + trades_df['date'] = trades_df['exit_time'].dt.date + + daily_profit = trades_df.groupby(['date', 'direction']).profit.sum().unstack(fill_value=0) + #print("dp",daily_profit) + daily_cumulative_profit = trades_df.groupby('date').profit.sum().cumsum() + + # Create the plot + fig, ax1 = plt.subplots(figsize=(10, 6)) + + # Bar chart for daily profit composition + daily_profit.plot(kind='bar', stacked=True, ax=ax1, color=['green', 'red'], zorder=2) + ax1.set_ylabel('Daily Profit') + ax1.set_xlabel('Date') + #ax1.xaxis.set_major_locator(MaxNLocator(10)) + + # Line chart for cumulative daily profit + #ax2 = ax1.twinx() + #print(daily_cumulative_profit) + #print(daily_cumulative_profit.index) + #ax2.plot(daily_cumulative_profit.index, daily_cumulative_profit, color='yellow', linestyle='-', linewidth=2, zorder=3) + #ax2.set_ylabel('Cumulative Profit') + + # Setting the secondary y-axis range dynamically based on cumulative profit values + # ax2.set_ylim(daily_cumulative_profit.min() - (daily_cumulative_profit.std() * 2), + # daily_cumulative_profit.max() + (daily_cumulative_profit.std() * 2)) + + # Dark mode settings + ax1.set_facecolor('black') + # ax1.grid(True) + #ax2.set_facecolor('black') + fig.patch.set_facecolor('black') + ax1.tick_params(colors='white') + #ax2.tick_params(colors='white') + # ax1.xaxis_date() + # ax1.xaxis.set_major_formatter(mdates.DateFormatter('%d.%m.', tz=zoneNY)) + ax1.tick_params(axis='x', rotation=45) + + # Footer + footer_text = f'Days Count: {days_cnt} | Parameters: {{"runner_ids": {len(runner_ids) if runner_ids is not None else None}, "batch_id": {batch_id}, "stream": {stream}}}' + plt.figtext(0.5, 0.01, footer_text, wrap=True, horizontalalignment='center', fontsize=8, color='white') + + # Save or stream the plot + if stream: + img_stream = BytesIO() + plt.savefig(img_stream, format='png', bbox_inches='tight', facecolor=fig.get_facecolor(), edgecolor='none') + img_stream.seek(0) + plt.close(fig) + return (0, img_stream) + else: + plt.savefig(f'{__name__}.png', bbox_inches='tight', facecolor=fig.get_facecolor(), edgecolor='none') + plt.close(fig) + return (0, None) + + except Exception as e: + # Detailed error reporting + return (-1, str(e) + format_exc()) +# Local debugging +if __name__ == '__main__': + batch_id = "6f9b012c" + res, val = daily_profit_distribution(batch_id=batch_id) + print(res, val) diff --git a/v2realbot/reporting/analyzer/find_optimal_cutoff.py b/v2realbot/reporting/analyzer/find_optimal_cutoff.py index ed997d1..c61cccf 100644 --- a/v2realbot/reporting/analyzer/find_optimal_cutoff.py +++ b/v2realbot/reporting/analyzer/find_optimal_cutoff.py @@ -25,7 +25,7 @@ from io import BytesIO # Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere #LOSS and PROFIT without GRAPH -def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: bool = False, rem_outliers:bool = False, file: str = "optimalcutoff.png",steps:int = 50): +def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: bool = False, mode:str="absolute", rem_outliers:bool = False, z_score_threshold:int = 3, file: str = "optimalcutoff.png",steps:int = 50): #TODO dopracovat drawdown a minimalni a maximalni profity nikoliv cumulovane, zamyslet se #TODO list of runner_ids @@ -115,7 +115,11 @@ def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: b for trade in trades: if trade.status == TradeStatus.CLOSED and trade.exit_time: day = trade.exit_time.date() - daily_cumulative_profits[day].append(trade.profit) + if mode == "absolute": + daily_cumulative_profits[day].append(trade.profit) + #relative profit + else: + daily_cumulative_profits[day].append(trade.rel_profit) for day in daily_cumulative_profits: daily_cumulative_profits[day] = np.cumsum(daily_cumulative_profits[day]) @@ -131,7 +135,7 @@ def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: b for day, profits in cumulative_profits.items(): if len(profits) > 0: day_z_score = z_scores[list(cumulative_profits.keys()).index(day)] - if abs(day_z_score) < 3: # Adjust threshold as needed + if abs(day_z_score) < z_score_threshold: # Adjust threshold as needed filtered_profits[day] = profits return filtered_profits @@ -145,26 +149,25 @@ def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: b # profit_range = (0, max_profit) if max_profit > 0 else (0, 0) # loss_range = (min_profit, 0) if min_profit < 0 else (0, 0) + if mode == "absolute": # OPT2 Calculate profit_range and loss_range based on all cumulative profits - all_cumulative_profits = np.concatenate([profits for profits in daily_cumulative_profits.values()]) - max_cumulative_profit = np.max(all_cumulative_profits) - min_cumulative_profit = np.min(all_cumulative_profits) - profit_range = (0, max_cumulative_profit) if max_cumulative_profit > 0 else (0, 0) - loss_range = (min_cumulative_profit, 0) if min_cumulative_profit < 0 else (0, 0) + all_cumulative_profits = np.concatenate([profits for profits in daily_cumulative_profits.values()]) + max_cumulative_profit = np.max(all_cumulative_profits) + min_cumulative_profit = np.min(all_cumulative_profits) + profit_range = (0, max_cumulative_profit) if max_cumulative_profit > 0 else (0, 0) + loss_range = (min_cumulative_profit, 0) if min_cumulative_profit < 0 else (0, 0) + else: + #for relative - hardcoded + profit_range = (0, 1) # Adjust based on your data + loss_range = (-1, 0) - print("Calculated ranges", profit_range, loss_range) + print("Ranges", profit_range, loss_range) num_points = steps # Adjust for speed vs accuracy profit_cutoffs = np.linspace(*profit_range, num_points) loss_cutoffs = np.linspace(*loss_range, num_points) - # OPT 3Statically define ranges for loss and profit cutoffs - # profit_range = (0, 1000) # Adjust based on your data - # loss_range = (-1000, 0) - # num_points = 20 # Adjust for speed vs accuracy - profit_cutoffs = np.linspace(*profit_range, num_points) - loss_cutoffs = np.linspace(*loss_range, num_points) total_profits_matrix = np.zeros((len(profit_cutoffs), len(loss_cutoffs))) @@ -207,12 +210,12 @@ def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: b } plt.rcParams.update(params) plt.figure(figsize=(10, 8)) - sns.heatmap(total_profits_matrix, xticklabels=np.rint(loss_cutoffs).astype(int), yticklabels=np.rint(profit_cutoffs).astype(int), cmap="viridis") + sns.heatmap(total_profits_matrix, xticklabels=np.rint(loss_cutoffs).astype(int) if mode == "absolute" else np.around(loss_cutoffs, decimals=3), yticklabels=np.rint(profit_cutoffs).astype(int) if mode == "absolute" else np.around(profit_cutoffs, decimals=3), cmap="viridis") plt.xticks(rotation=90) # Rotate x-axis labels to be vertical plt.yticks(rotation=0) # Keep y-axis labels horizontal plt.gca().invert_yaxis() plt.gca().invert_xaxis() - plt.suptitle(f"Total Profit for Combinations of Profit/Loss Cutoffs ({cnt_max})", fontsize=16) + plt.suptitle(f"Total {mode} Profit for Profit/Loss Cutoffs ({cnt_max})", fontsize=16) plt.title(f"Optimal Profit Cutoff: {optimal_profit_cutoff:.2f}, Optimal Loss Cutoff: {optimal_loss_cutoff:.2f}, Max Profit: {max_profit:.2f}", fontsize=10) plt.xlabel("Loss Cutoff") plt.ylabel("Profit Cutoff") @@ -236,8 +239,8 @@ if __name__ == '__main__': # id_list = ["e8938b2e-8462-441a-8a82-d823c6a025cb"] # generate_trading_report_image(runner_ids=id_list) batch_id = "c76b4414" - vstup = AnalyzerInputs(**params) - res, val = find_optimal_cutoff(batch_id=batch_id, file="optimal_cutoff_vectorized.png",steps=20) + #vstup = AnalyzerInputs(**params) + res, val = find_optimal_cutoff(batch_id=batch_id, mode="relative", z_score_threshold=2, file="optimal_cutoff_vectorized.png",steps=20) #res, val = find_optimal_cutoff(batch_id=batch_id, rem_outliers=True, file="optimal_cutoff_vectorized_nooutliers.png") print(res,val) \ No newline at end of file diff --git a/v2realbot/reporting/analyzer/find_optimal_cutoff_REL.py b/v2realbot/reporting/analyzer/find_optimal_cutoff_REL.py new file mode 100644 index 0000000..8eff788 --- /dev/null +++ b/v2realbot/reporting/analyzer/find_optimal_cutoff_REL.py @@ -0,0 +1,244 @@ +import matplotlib +import matplotlib.dates as mdates +#matplotlib.use('Agg') # Set the Matplotlib backend to 'Agg' +import matplotlib.pyplot as plt +import seaborn as sns +import pandas as pd +from datetime import datetime +from typing import List +from enum import Enum +import numpy as np +import v2realbot.controller.services as cs +from rich import print +from v2realbot.common.model import AnalyzerInputs +from v2realbot.common.PrescribedTradeModel import TradeDirection, TradeStatus, Trade, TradeStoplossType +from v2realbot.utils.utils import isrising, isfalling,zoneNY, price2dec, safe_get#, print +from pathlib import Path +from v2realbot.config import WEB_API_KEY, DATA_DIR, MEDIA_DIRECTORY +from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account, OrderSide +from io import BytesIO +from v2realbot.utils.historicals import get_historical_bars +from alpaca.data.timeframe import TimeFrame, TimeFrameUnit +from collections import defaultdict +from scipy.stats import zscore +from io import BytesIO +# Assuming Trade, TradeStatus, TradeDirection, TradeStoplossType classes are defined elsewhere + +#HEATMAPA pro RELATIVNI PROFIT - WIP +#po dodelani dat do stejné funkce jen s parametrem typ +def find_optimal_cutoff(runner_ids: list = None, batch_id: str = None, stream: bool = False, rem_outliers:bool = False, z_score_threshold:int = 3, file: str = "optimalcutoff.png",steps:int = 50): + + #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 = [] + cnt_max = len(runner_ids) + cnt = 0 + #zatim zjistujeme start a end z min a max dni - jelikoz muze byt i seznam runner_ids a nejenom batch + end_date = None + start_date = None + for id in runner_ids: + cnt += 1 + #get runner + res, sada =cs.get_archived_runner_header_byID(id) + if res != 0: + print(f"no runner {id} found") + return -1, f"no runner {id} found" + + #print("archrunner") + #print(sada) + + if cnt == 1: + start_date = sada.bt_from if sada.mode in [Mode.BT,Mode.PREP] else sada.started + if cnt == cnt_max: + end_date = sada.bt_to if sada.mode in [Mode.BT or Mode.PREP] else sada.stopped + # Parse trades + + trades_dicts = sada.metrics["prescr_trades"] + + for trade_dict in trades_dicts: + trade_dict['last_update'] = datetime.fromtimestamp(trade_dict.get('last_update')).astimezone(zoneNY) if trade_dict['last_update'] is not None else None + trade_dict['entry_time'] = datetime.fromtimestamp(trade_dict.get('entry_time')).astimezone(zoneNY) if trade_dict['entry_time'] is not None else None + trade_dict['exit_time'] = datetime.fromtimestamp(trade_dict.get('exit_time')).astimezone(zoneNY) if trade_dict['exit_time'] is not None else None + trades.append(Trade(**trade_dict)) + + #print(trades) + + # symbol = sada.symbol + # #hour bars for backtested period + # print(start_date,end_date) + # bars= get_historical_bars(symbol, start_date, end_date, TimeFrame.Hour) + # print("bars for given period",bars) + # """Bars a dictionary with the following keys: + # * high: A list of high prices + # * low: A list of low prices + # * volume: A list of volumes + # * close: A list of close prices + # * hlcc4: A list of HLCC4 indicators + # * open: A list of open prices + # * time: A list of times in UTC (ISO 8601 format) + # * trades: A list of number of trades + # * resolution: A list of resolutions (all set to 'D') + # * confirmed: A list of booleans (all set to True) + # * vwap: A list of VWAP indicator + # * updated: A list of booleans (all set to True) + # * index: A list of integers (from 0 to the length of the list of daily bars) + # """ + + # Filter to only use trades with status 'CLOSED' + closed_trades = [trade for trade in trades if trade.status == TradeStatus.CLOSED] + + #print(closed_trades) + + if len(closed_trades) == 0: + return -1, "image generation no closed trades" + + # # Group trades by date and calculate daily profits + # trades_by_day = defaultdict(list) + # for trade in trades: + # if trade.status == TradeStatus.CLOSED and trade.exit_time: + # trade_day = trade.exit_time.date() + # trades_by_day[trade_day].append(trade) + + # Precompute daily cumulative profits + daily_cumulative_profits = defaultdict(list) + for trade in trades: + if trade.status == TradeStatus.CLOSED and trade.exit_time: + day = trade.exit_time.date() + daily_cumulative_profits[day].append(trade.profit) + + for day in daily_cumulative_profits: + daily_cumulative_profits[day] = np.cumsum(daily_cumulative_profits[day]) + + + if rem_outliers: + # Remove outliers based on z-scores + def remove_outliers(cumulative_profits): + all_profits = [profit[-1] for profit in cumulative_profits.values() if len(profit) > 0] + z_scores = zscore(all_profits) + print(z_scores) + filtered_profits = {} + for day, profits in cumulative_profits.items(): + if len(profits) > 0: + day_z_score = z_scores[list(cumulative_profits.keys()).index(day)] + if abs(day_z_score) < z_score_threshold: # Adjust threshold as needed + filtered_profits[day] = profits + return filtered_profits + + daily_cumulative_profits = remove_outliers(daily_cumulative_profits) + + + # OPT1 Dynamically calculate profit_range and loss_range - based on eod daily profit + # all_final_profits = [profits[-1] for profits in daily_cumulative_profits.values() if len(profits) > 0] + # max_profit = max(all_final_profits) + # min_profit = min(all_final_profits) + # profit_range = (0, max_profit) if max_profit > 0 else (0, 0) + # loss_range = (min_profit, 0) if min_profit < 0 else (0, 0) + + # OPT2 Calculate profit_range and loss_range based on all cumulative profits + all_cumulative_profits = np.concatenate([profits for profits in daily_cumulative_profits.values()]) + max_cumulative_profit = np.max(all_cumulative_profits) + min_cumulative_profit = np.min(all_cumulative_profits) + profit_range = (0, max_cumulative_profit) if max_cumulative_profit > 0 else (0, 0) + loss_range = (min_cumulative_profit, 0) if min_cumulative_profit < 0 else (0, 0) + + print("Calculated ranges", profit_range, loss_range) + + num_points = steps # Adjust for speed vs accuracy + profit_cutoffs = np.linspace(*profit_range, num_points) + loss_cutoffs = np.linspace(*loss_range, num_points) + + # OPT 3Statically define ranges for loss and profit cutoffs + # profit_range = (0, 1000) # Adjust based on your data + # loss_range = (-1000, 0) + # num_points = 20 # Adjust for speed vs accuracy + + profit_cutoffs = np.linspace(*profit_range, num_points) + loss_cutoffs = np.linspace(*loss_range, num_points) + + total_profits_matrix = np.zeros((len(profit_cutoffs), len(loss_cutoffs))) + + for i, profit_cutoff in enumerate(profit_cutoffs): + for j, loss_cutoff in enumerate(loss_cutoffs): + total_profit = 0 + for daily_profit in daily_cumulative_profits.values(): + cutoff_index = np.where((daily_profit >= profit_cutoff) | (daily_profit <= loss_cutoff))[0] + if cutoff_index.size > 0: + total_profit += daily_profit[cutoff_index[0]] + else: + total_profit += daily_profit[-1] if daily_profit.size > 0 else 0 + total_profits_matrix[i, j] = total_profit + + # Find the optimal combination + optimal_idx = np.unravel_index(total_profits_matrix.argmax(), total_profits_matrix.shape) + optimal_profit_cutoff = profit_cutoffs[optimal_idx[0]] + optimal_loss_cutoff = loss_cutoffs[optimal_idx[1]] + max_profit = total_profits_matrix[optimal_idx] + + # Plotting + # Setting up dark mode for the plots + plt.style.use('dark_background') + + # Optionally, you can further customize colors, labels, and axes + params = { + 'axes.titlesize': 9, + 'axes.labelsize': 8, + 'xtick.labelsize': 9, + 'ytick.labelsize': 9, + 'axes.labelcolor': '#a9a9a9', #a1a3aa', + 'axes.facecolor': '#121722', #'#0e0e0e', #202020', # Dark background for plot area + 'axes.grid': False, # Turn off the grid globally + 'grid.color': 'gray', # If the grid is on, set grid line color + 'grid.linestyle': '--', # Grid line style + 'grid.linewidth': 1, + 'xtick.color': '#a9a9a9', + 'ytick.color': '#a9a9a9', + 'axes.edgecolor': '#a9a9a9' + } + plt.rcParams.update(params) + plt.figure(figsize=(10, 8)) + sns.heatmap(total_profits_matrix, xticklabels=np.rint(loss_cutoffs).astype(int), yticklabels=np.rint(profit_cutoffs).astype(int), cmap="viridis") + plt.xticks(rotation=90) # Rotate x-axis labels to be vertical + plt.yticks(rotation=0) # Keep y-axis labels horizontal + plt.gca().invert_yaxis() + plt.gca().invert_xaxis() + plt.suptitle(f"Total Profit for Combinations of Profit/Loss Cutoffs ({cnt_max})", fontsize=16) + plt.title(f"Optimal Profit Cutoff: {optimal_profit_cutoff:.2f}, Optimal Loss Cutoff: {optimal_loss_cutoff:.2f}, Max Profit: {max_profit:.2f}", fontsize=10) + plt.xlabel("Loss Cutoff") + plt.ylabel("Profit Cutoff") + + if stream is False: + plt.savefig(file) + plt.close() + print(f"Optimal Profit Cutoff(rem_outliers:{rem_outliers}): {optimal_profit_cutoff}, Optimal Loss Cutoff: {optimal_loss_cutoff}, Max Profit: {max_profit}") + return 0, None + else: + # Return the image as a BytesIO stream + img_stream = BytesIO() + plt.savefig(img_stream, format='png') + plt.close() + img_stream.seek(0) # Rewind the stream to the beginning + return 0, img_stream + +# Example usage +# trades = [list of Trade objects] +if __name__ == '__main__': + # id_list = ["e8938b2e-8462-441a-8a82-d823c6a025cb"] + # generate_trading_report_image(runner_ids=id_list) + batch_id = "c76b4414" + vstup = AnalyzerInputs(**params) + res, val = find_optimal_cutoff(batch_id=batch_id, file="optimal_cutoff_vectorized.png",steps=20) + #res, val = find_optimal_cutoff(batch_id=batch_id, rem_outliers=True, file="optimal_cutoff_vectorized_nooutliers.png") + + print(res,val) \ No newline at end of file diff --git a/v2realbot/reporting/analyzer/summarize_trade_metrics.py b/v2realbot/reporting/analyzer/summarize_trade_metrics.py new file mode 100644 index 0000000..2bec57c --- /dev/null +++ b/v2realbot/reporting/analyzer/summarize_trade_metrics.py @@ -0,0 +1,129 @@ +import matplotlib +import matplotlib.dates as mdates +matplotlib.use('Agg') # Set the Matplotlib backend to 'Agg' +import matplotlib.pyplot as plt +from matplotlib.ticker import MaxNLocator +import seaborn as sns +import pandas as pd +from datetime import datetime +from typing import List +from enum import Enum +import numpy as np +import v2realbot.controller.services as cs +from rich import print +from v2realbot.common.model import AnalyzerInputs +from v2realbot.common.PrescribedTradeModel import TradeDirection, TradeStatus, Trade, TradeStoplossType +from v2realbot.utils.utils import isrising, isfalling,zoneNY, price2dec, safe_get#, print +from pathlib import Path +from v2realbot.config import WEB_API_KEY, DATA_DIR, MEDIA_DIRECTORY +from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account, OrderSide +from io import BytesIO +from v2realbot.utils.historicals import get_historical_bars +from alpaca.data.timeframe import TimeFrame, TimeFrameUnit +from collections import defaultdict +from scipy.stats import zscore +from io import BytesIO +from v2realbot.reporting.load_trades import load_trades +from typing import Tuple, Optional, List +from traceback import format_exc +import pandas as pd + + +def summarize_trade_metrics(runner_ids: list = None, batch_id: str = None, stream: bool = False): + try: + res, trades, days_cnt = load_trades(runner_ids, batch_id) + if res != 0: + raise Exception("Error in loading trades") + + closed_trades = [trade for trade in trades if trade.status == "closed"] + + # Calculate metrics + metrics = calculate_metrics(closed_trades) + + # Generate and process image + img_stream = generate_table_image(metrics) + + # Add footer to image + #img_stream = add_footer_to_image(img_stream, days_cnt, runner_ids, batch_id, stream) + + # Output handling + if stream: + img_stream.seek(0) + return (0, img_stream) + else: + with open(f'summarize_trade_metrics_{batch_id}.png', 'wb') as f: + f.write(img_stream.getbuffer()) + return (0, None) + + except Exception as e: + # Detailed error reporting + return (-1, str(e)+format_exc()) + +def calculate_metrics(closed_trades): + if not closed_trades: + return {} + + total_profit = sum(trade.profit for trade in closed_trades) + max_profit = max(trade.profit for trade in closed_trades) + min_profit = min(trade.profit for trade in closed_trades) + total_trades = len(closed_trades) + long_trades = sum(1 for trade in closed_trades if trade.direction == "long") + short_trades = sum(1 for trade in closed_trades if trade.direction == "short") + + # Daily Metrics Calculation + trades_by_day = {} + for trade in closed_trades: + day = trade.entry_time.date() if trade.entry_time else None + if day: + trades_by_day.setdefault(day, []).append(trade) + + avg_trades_per_day = sum(len(trades) for trades in trades_by_day.values()) / len(trades_by_day) + avg_long_trades_per_day = sum(sum(1 for trade in trades if trade.direction == "long") for trades in trades_by_day.values()) / len(trades_by_day) + avg_short_trades_per_day = sum(sum(1 for trade in trades if trade.direction == "short") for trades in trades_by_day.values()) / len(trades_by_day) + + return { + "Average Profit": total_profit / total_trades, + "Maximum Profit": max_profit, + "Minimum Profit": min_profit, + "Total Number of Trades": total_trades, + "Number of Long Trades": long_trades, + "Number of Short Trades": short_trades, + "Average Trades per Day": avg_trades_per_day, + "Average Long Trades per Day": avg_long_trades_per_day, + "Average Short Trades per Day": avg_short_trades_per_day + } + +def generate_table_image(metrics): + fig, ax = plt.subplots(figsize=(10, 6)) + ax.axis('tight') + ax.axis('off') + + # Convert metrics to a 2D array where each row is a list + cell_text = [[value] for value in metrics.values()] + + # Convert dict keys to a list for row labels + row_labels = list(metrics.keys()) + + ax.table(cellText=cell_text, + rowLabels=row_labels, + loc='center') + + plt.subplots_adjust(left=0.2, top=0.8) + plt.title("Trade Metrics Summary", color='white') + + img_stream = BytesIO() + plt.savefig(img_stream, format='png', bbox_inches='tight', pad_inches=0.1, facecolor='black') + plt.close(fig) + return img_stream + +def add_footer_to_image(img_stream, days_cnt, runner_ids, batch_id, stream): + # Implementation for adding a footer to the image + # This can be done using PIL (Python Imaging Library) or other image processing libraries + # For simplicity, I'm leaving this as a placeholder + pass + +# Local debugging +if __name__ == '__main__': + batch_id = "73ad1866" + res, val = summarize_trade_metrics(batch_id=batch_id) + print(res, val) diff --git a/v2realbot/static/index.html b/v2realbot/static/index.html index 94d5971..6033364 100644 --- a/v2realbot/static/index.html +++ b/v2realbot/static/index.html @@ -225,7 +225,7 @@ -