diff --git a/v2realbot/ENTRY_ClassicSL_v01.py b/v2realbot/ENTRY_ClassicSL_v01.py index 3a3ba5f..5314e07 100644 --- a/v2realbot/ENTRY_ClassicSL_v01.py +++ b/v2realbot/ENTRY_ClassicSL_v01.py @@ -6,7 +6,7 @@ from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account, Orde from v2realbot.indicators.indicators import ema from v2realbot.indicators.oscillators import rsi from v2realbot.common.PrescribedTradeModel import Trade, TradeDirection, TradeStatus, TradeStoplossType -from v2realbot.utils.utils import ltp, isrising, isfalling,trunc,AttributeDict, zoneNY, price2dec, print, safe_get, round2five, is_open_rush, is_close_rush, eval_cond_dict, Average, crossed_down, crossed_up, crossed, is_pivot, json_serial +from v2realbot.utils.utils import ltp, isrising, isfalling,trunc,AttributeDict, zoneNY, price2dec, print, safe_get, round2five, is_open_rush, is_close_rush, is_window_open, eval_cond_dict, Average, crossed_down, crossed_up, crossed, is_pivot, json_serial from v2realbot.utils.directive_utils import get_conditions_from_configuration from v2realbot.common.model import SLHistory from datetime import datetime @@ -1108,13 +1108,17 @@ def next(data, state: StrategyState): #obecne precondition preds vstupem - platne jak pro condition based tak pro plugin def common_go_preconditions_check(signalname: str, options: dict): #ZAKLADNI KONTROLY ATRIBUTU s fallbackem na obecné - #check working windows - close_rush = safe_get(options, "close_rush",safe_get(state.vars, "open_rush",0)) - open_rush = safe_get(options, "open_rush",safe_get(state.vars, "close_rush",0)) + #check working windows (open - close, in minutes from the start of marker) + window_open = safe_get(options, "window_open",safe_get(state.vars, "window_open",0)) + window_close = safe_get(options, "window_close",safe_get(state.vars, "window_close",390)) - if is_open_rush(datetime.fromtimestamp(data['updated']).astimezone(zoneNY), open_rush) or is_close_rush(datetime.fromtimestamp(data['updated']).astimezone(zoneNY), close_rush): - state.ilog(e=f"SIGNAL {signalname} - RUSH STANDBY", msg=f"{open_rush=} {close_rush=} ") - return False + if is_window_open(datetime.fromtimestamp(data['updated']).astimezone(zoneNY), window_open, window_close) is False: + state.ilog(e=f"SIGNAL {signalname} - WINDOW CLOSED", msg=f"{window_open=} {window_close=} ") + return False + + # if is_open_rush(datetime.fromtimestamp(data['updated']).astimezone(zoneNY), open_rush) or is_close_rush(datetime.fromtimestamp(data['updated']).astimezone(zoneNY), close_rush): + # state.ilog(e=f"SIGNAL {signalname} - WINDOW CLOSED", msg=f"{open_rush=} {close_rush=} ") + # return False #natvrdo nebo na podminku activated = safe_get(options, "activated", True) diff --git a/v2realbot/common/__pycache__/model.cpython-310.pyc b/v2realbot/common/__pycache__/model.cpython-310.pyc index 62fc75b..6329a60 100644 Binary files a/v2realbot/common/__pycache__/model.cpython-310.pyc and b/v2realbot/common/__pycache__/model.cpython-310.pyc differ diff --git a/v2realbot/common/model.py b/v2realbot/common/model.py index 36af815..dec2925 100644 --- a/v2realbot/common/model.py +++ b/v2realbot/common/model.py @@ -27,6 +27,7 @@ from alpaca.data.enums import Exchange class Intervals(BaseModel): start: str end: str + note: Optional[str] = None # Define the data model for the TestLists class TestList(BaseModel): @@ -71,6 +72,7 @@ class RunRequest(BaseModel): ilog_save: bool = False bt_from: datetime = None bt_to: datetime = None + test_batch_id: Optional[str] = None cash: int = 100000 diff --git a/v2realbot/controller/services.py b/v2realbot/controller/services.py index fbc923b..4ad56c4 100644 --- a/v2realbot/controller/services.py +++ b/v2realbot/controller/services.py @@ -6,7 +6,7 @@ from alpaca.data.requests import StockTradesRequest, StockBarsRequest from alpaca.data.enums import DataFeed from alpaca.data.timeframe import TimeFrame from v2realbot.enums.enums import RecordType, StartBarAlign, Mode, Account, OrderSide -from v2realbot.common.model import StrategyInstance, Runner, RunRequest, RunArchive, RunArchiveDetail, RunArchiveChange, Bar, TradeEvent +from v2realbot.common.model import StrategyInstance, Runner, RunRequest, RunArchive, RunArchiveDetail, RunArchiveChange, Bar, TradeEvent, TestList, Intervals from v2realbot.utils.utils import AttributeDict, zoneNY, dict_replace_value, Store, parse_toml_string, json_serial, is_open_hours, send_to_telegram from v2realbot.utils.ilog import delete_logs from v2realbot.common.PrescribedTradeModel import Trade, TradeDirection, TradeStatus, TradeStoplossType @@ -19,6 +19,7 @@ from tinydb import TinyDB, Query, where from tinydb.operations import set import json from numpy import ndarray +from rich import print import pandas as pd from traceback import format_exc from datetime import timedelta, time @@ -309,8 +310,94 @@ def capsule(target: object, db: object): db.runners.remove(i) print("Runner STOPPED") + +#vrátí konkrétní sadu testlistu +def get_testlist_byID(record_id: str): + conn = pool.get_connection() + try: + cursor = conn.cursor() + cursor.execute("SELECT id, name, dates FROM test_list WHERE id = ?", (record_id,)) + row = cursor.fetchone() + finally: + pool.release_connection(conn) + + if row is None: + return -2, "not found" + else: + return 0, TestList(id=row[0], name=row[1], dates=json.loads(row[2])) + + +#volano pro batchove spousteni (BT,) +def run_batch_stratin(id: UUID, runReq: RunRequest): + if runReq.test_batch_id is None: + return (-1, "batch_id required for batch run") + + if runReq.mode != Mode.BT: + return (-1, "batch run only for backtest") + + print("request values:", runReq) + + print("getting intervals") + testlist: TestList + + res, testlist = get_testlist_byID(record_id=runReq.test_batch_id) + + if res < 0: + return (-1, f"not existing ID of testlists with {runReq.test_batch_id}") + +#spousti se vlakno s paralelnim behem a vracime ok + ridici_vlakno = Thread(target=batch_run_manager, args=(id, runReq, testlist), name=f"Batch run controll thread started.") + ridici_vlakno.start() + print(enumerate()) + + return 0, f"Batch run started" + + +#thread, ktery bude ridit paralelni spousteni +# bud ceka na dokonceni v runners nebo to bude ridit jinak a bude mit jednoho runnera? +# nejak vymyslet. +# logovani zatim jen do print +def batch_run_manager(id: UUID, runReq: RunRequest, testlist: TestList): + #zde muzu iterovat nad intervaly + #cekat az dobehne jeden interval a pak spustit druhy + #pripadne naplanovat beh - to uvidim + #domyslet kompatibilitu s budoucim automatickym "dennim" spousteni strategii + #taky budu mit nejaky konfiguracni RUN MANAGER, tak by krome rizeniho denniho runu + #mohl podporovat i BATCH RUNy. + batch_id = str(uuid4())[:8] + print("generated batch_ID", batch_id) + + print("test batch", testlist) + + print("test date", testlist.dates) + interval: Intervals + cnt_max = len(testlist.dates) + cnt = 0 + for intrvl in testlist.dates: + cnt += 1 + interval = intrvl + if interval.note is not None: + print("mame zde note") + print("Datum od", interval.start) + print("Datum do", interval.end) + print("starting") + + #předání atributů datetime.fromisoformat + runReq.bt_from = datetime.fromisoformat(interval.start) + runReq.bt_to = datetime.fromisoformat(interval.end) + runReq.note = f"Batch {batch_id} run #{cnt}/{cnt_max} Note:{interval.note}" + + #protoze jsme v ridicim vlaknu, poustime za sebou jednotlive stratiny v synchronnim modu + res, id_val = run_stratin(id=id, runReq=runReq, synchronous=True) + if res < 0: + print(f"CHyba v runu #{cnt} od:{runReq.bt_from} do {runReq.bt_to} -> {id_val}") + break + + print("Batch manager FINISHED") + + #stratin run -def run_stratin(id: UUID, runReq: RunRequest): +def run_stratin(id: UUID, runReq: RunRequest, synchronous: bool = False): if runReq.mode == Mode.BT: if runReq.bt_from is None: return (-1, "start date required for BT") @@ -405,6 +492,12 @@ def run_stratin(id: UUID, runReq: RunRequest): print(db.runners) print(i) print(enumerate()) + + #pokud spoustime v batch módu, tak čekáme na výsledek a pak pouštíme další run + if synchronous: + print(f"waiting for thread {vlakno} to finish") + vlakno.join() + return (0, id) except Exception as e: return (-2, "Exception: "+str(e)+format_exc()) diff --git a/v2realbot/loader/__pycache__/aggregator.cpython-310.pyc b/v2realbot/loader/__pycache__/aggregator.cpython-310.pyc index 2d13f6f..74bdcc9 100644 Binary files a/v2realbot/loader/__pycache__/aggregator.cpython-310.pyc and b/v2realbot/loader/__pycache__/aggregator.cpython-310.pyc differ diff --git a/v2realbot/main.py b/v2realbot/main.py index 9c51c97..d728bca 100644 --- a/v2realbot/main.py +++ b/v2realbot/main.py @@ -226,7 +226,11 @@ def _get_stratin(stratin_id) -> StrategyInstance: @app.put("/stratins/{stratin_id}/run", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK) def _run_stratin(stratin_id: UUID, runReq: RunRequest): - res, id = cs.run_stratin(id=stratin_id, runReq=runReq) + print(runReq) + if runReq.test_batch_id is not None: + res, id = cs.run_batch_stratin(id=stratin_id, runReq=runReq) + else: + res, id = cs.run_stratin(id=stratin_id, runReq=runReq) if res == 0: return id elif res < 0: raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error: {res}:{id}") @@ -336,8 +340,8 @@ def _get_alpaca_history_bars(symbol: str, datetime_object_from: datetime, dateti @app.post('/testlists/', dependencies=[Depends(api_key_auth)]) def create_record(testlist: TestList): # Generate a new UUID for the record - testlist.id = str(uuid4()) - + testlist.id = str(uuid4())[:8] + # Insert the record into the database conn = pool.get_connection() cursor = conn.cursor() @@ -366,17 +370,12 @@ def get_testlists(): # API endpoint to retrieve a single record by ID @app.get('/testlists/{record_id}') def get_testlist(record_id: str): - conn = pool.get_connection() - cursor = conn.cursor() - cursor.execute("SELECT id, name, dates FROM test_list WHERE id = ?", (record_id,)) - row = cursor.fetchone() - pool.release_connection(conn) - - if row is None: + res, testlist = cs.get_testlist_byID(record_id=record_id) + + if res == 0: + return testlist + elif res < 0: raise HTTPException(status_code=404, detail='Record not found') - - testlist = TestList(id=row[0], name=row[1], dates=json.loads(row[2])) - return testlist # API endpoint to update a record @app.put('/testlists/{record_id}') diff --git a/v2realbot/static/index.html b/v2realbot/static/index.html index dd60301..7555b30 100644 --- a/v2realbot/static/index.html +++ b/v2realbot/static/index.html @@ -38,7 +38,10 @@ + + +
@@ -112,6 +115,7 @@
+
status info
@@ -340,7 +344,16 @@
-
+ + + +
+
+
+ + + +
@@ -459,7 +472,6 @@
-
@@ -472,6 +484,10 @@
+
+ + +
@@ -521,6 +537,8 @@ + +
diff --git a/v2realbot/static/js/archivechart.js b/v2realbot/static/js/archivechart.js index 69c049b..2c1f89a 100644 --- a/v2realbot/static/js/archivechart.js +++ b/v2realbot/static/js/archivechart.js @@ -24,6 +24,8 @@ function transform_data(data) { var sl_line_markers_sada = [] //console.log(JSON.stringify(data.ext_data.sl_history, null, 2)) prev_id = 0 + //cas of first record, nekdy jsou stejny - musim pridat setinku + prev_cas = 0 data.ext_data.sl_history.forEach((histRecord, index, array) => { console.log("plnime") @@ -41,8 +43,16 @@ function transform_data(data) { } prev_id = histRecord.id + //prevedeme iso data na timestampy cas = histRecord.time + + if (cas == prev_cas) { + cas = cas + 0.001 + } + + prev_cas = cas + //line pro buy/sell markery sline = {} sline["time"] = cas @@ -599,6 +609,8 @@ function chart_archived_run(archRecord, data, oneMinuteBars) { slLine.forEach((series, index, array) => { chart.removeSeries(series) }) + slLine=[] + } // if (slLine) { // chart.removeSeries(slLine) @@ -627,7 +639,6 @@ function chart_archived_run(archRecord, data, oneMinuteBars) { }); slLine_temp.setData(slRecord); - //zatim nemame markery pro sl history slLine_temp.setMarkers(transformed_data["sl_line_markers"][index]); slLine.push(slLine_temp) diff --git a/v2realbot/static/js/mytables.js b/v2realbot/static/js/mytables.js index 8895fa1..cd9f8b1 100644 --- a/v2realbot/static/js/mytables.js +++ b/v2realbot/static/js/mytables.js @@ -406,11 +406,32 @@ $(document).ready(function () { //console.log(localStorage.getItem("bt_from")) $('#bt_to').val(localStorage.getItem("bt_to")); //console.log(localStorage.getItem("bt_to")) + $('#test_batch_id').val(localStorage.getItem("test_batch_id")); $('#mode').val(localStorage.getItem("mode")); $('#account').val(localStorage.getItem("account")); $('#debug').val(localStorage.getItem("debug")); $('#ilog_save').val(localStorage.getItem("ilog_save")); $('#runid').val(row.id); + + // Initially, check the value of "batch" and enable/disable "from" and "to" accordingly + if ($("#test_batch_id").val() !== "") { + $("#bt_from, #bt_to").prop("disabled", true); + } else { + $("#bt_from, #bt_to").prop("disabled", false); + } + + // Listen for changes in the "batch" input + $("#test_batch_id").on("input", function() { + if ($(this).val() !== "") { + // If "batch" is not empty, disable "from" and "to" + $("#bt_from, #bt_to").prop("disabled", true); + } else { + // If "batch" is empty, enable "from" and "to" + $("#bt_from, #bt_to").prop("disabled", false); + } + }); + + }); //button add @@ -441,6 +462,23 @@ $(document).ready(function () { $('.modal-title').html(" Edit Records"); $('#action').val('updateRecord'); $('#save').val('Save'); + //code for toml editor + // Initialize Monaco Editor + + // require.config({ paths: { vs: 'monaco-editor/min/vs' } }); + // require(['vs/editor/editor.main'], function () { + // var editor = $("#editor").monacoEditor({ + // language: "toml", + // value: row.stratvars_conf + // }); + // // Get content from Monaco Editor and set it to the textarea using jQuery + // editor.getModels()[0].onDidChangeContent(function() { + // var tomlContent = monaco.editor.getModels()[0].getValue(); + // $('#stratvars_conf').val(tomlContent); + // }); + // }); + + }); //delete button $('#button_delete').click(function () { @@ -576,6 +614,7 @@ var runnerRecords = $("#runModal").on('submit','#runForm', function(event){ localStorage.setItem("bt_from", $('#bt_from').val()); localStorage.setItem("bt_to", $('#bt_to').val()); + localStorage.setItem("test_batch_id", $('#test_batch_id').val()); localStorage.setItem("mode", $('#mode').val()); localStorage.setItem("account", $('#account').val()); localStorage.setItem("debug", $('#debug').val()); @@ -587,7 +626,7 @@ $("#runModal").on('submit','#runForm', function(event){ //rename runid to id Object.defineProperty(formData, "id", Object.getOwnPropertyDescriptor(formData, "runid")); delete formData["runid"]; - //console.log(formData) + console.log(formData) if ($('#ilog_save').prop('checked')) { formData.ilog_save = true; } @@ -599,6 +638,8 @@ $("#runModal").on('submit','#runForm', function(event){ if (formData.bt_from == "") {delete formData["bt_from"];} if (formData.bt_to == "") {delete formData["bt_to"];} + if (formData.test_batch_id == "") {delete formData["test_batch_id"];} + //create strat_json - snapshot of stratin row = stratinRecords.row('.selected').data(); const rec = new Object() diff --git a/v2realbot/static/js/testlist.js b/v2realbot/static/js/testlist.js index 2223df8..1e03144 100644 --- a/v2realbot/static/js/testlist.js +++ b/v2realbot/static/js/testlist.js @@ -16,7 +16,15 @@ $(document).ready(function() { datesArray.forEach(function(dates) { var tag = $('
' + dates.start + " --- " + dates.end + 'X
'); tag.find('.close').click(function() { + var dateText = tag.text(); + console.log("clicked") + datesArray = datesArray.filter(function(date) { + tagcontent = date.start + " --- " + date.end + "X" + console.log(tagcontent,dateText) + return tagcontent !== dateText; + }); $(this).parent().remove(); + console.log("ccclickd") }); $('#tagContainer').append(tag); }); @@ -29,9 +37,13 @@ $(document).ready(function() { records.forEach(function(record) { var recordItem = $('
'); var recordDetails = $('
').html('ID: ' + record.id + '
Name: ' + record.name + '
Dates: '); - + record.dates.forEach(function(interval) { - var intervalItem = $('
').html('Start: ' + interval.start + '
End: ' + interval.end); + var note = "" + if (interval.note !== null) { + var note = '
Note: ' + interval.note + } + var intervalItem = $('
').html('Start: ' + interval.start + '
End: ' + interval.end + note); recordDetails.append(intervalItem); }); @@ -98,15 +110,24 @@ $(document).ready(function() { $('#addTagBtn').click(function() { var dateTextStart = $('#datepickerstart').val().trim(); var dateTextEnd = $('#datepickerend').val().trim(); + var datenote = $('#datenote').val(); if ((dateTextStart !== '') && (dateTextEnd !== '')) { var tag = $('
' + dateTextStart + " --- " + dateTextEnd + 'X
'); tag.find('.close').click(function() { + var dateText = tag.text(); + console.log("clicked") + datesArray = datesArray.filter(function(date) { + tagcontent = date.start + " --- " + date.end + "X" + console.log(tagcontent,dateText) + return tagcontent !== dateText; + }); $(this).parent().remove(); }); $('#tagContainer').append(tag); var interval = {} interval["start"] = dateTextStart interval["end"] = dateTextEnd + interval["note"] = datenote datesArray.push(interval); $('#datepicker').val(''); } @@ -145,15 +166,17 @@ $(document).ready(function() { cancelEdit(); }); - $('#tagContainer').on('click', '.tag .close', function() { - var tag = $(this).parent(); - var dateText = tag.text(); - datesArray = datesArray.filter(function(date) { - tagcontent = date.start + " --- " + date.end - return tagcontent !== dateText; - }); - tag.remove(); - }); + // $('#tagContainer').on('click', '.tag .close', function() { + // var tag = $(this).parent(); + // var dateText = tag.text(); + // console.log("clicked") + // datesArray = datesArray.filter(function(date) { + // tagcontent = date.start + " --- " + date.end + // console.log(tagcontent,dateText) + // return tagcontent !== dateText; + // }); + // tag.remove(); + // }); function getRecords() { $.ajax({ diff --git a/v2realbot/static/js/utils.js b/v2realbot/static/js/utils.js index 32dab65..5e468a0 100644 --- a/v2realbot/static/js/utils.js +++ b/v2realbot/static/js/utils.js @@ -223,6 +223,7 @@ function cleanup_chart() { clear_status_header() chart = null indList = []; + slLine = [] markersLine = null avgBuyLine = null volumeSeries = null diff --git a/v2realbot/static/main.css b/v2realbot/static/main.css index 3b5d016..ebb3394 100644 --- a/v2realbot/static/main.css +++ b/v2realbot/static/main.css @@ -398,6 +398,13 @@ pre { 'opsz' 24 } +.intervalContainer { + float: left; + border-width: thick; + border: beige; + display: inline-table; + padding: 10px; +} /* TestList part generated by ChatGPT */ .recordItem { diff --git a/v2realbot/utils/__pycache__/utils.cpython-310.pyc b/v2realbot/utils/__pycache__/utils.cpython-310.pyc index a1c29e1..03aab20 100644 Binary files a/v2realbot/utils/__pycache__/utils.cpython-310.pyc and b/v2realbot/utils/__pycache__/utils.cpython-310.pyc differ diff --git a/v2realbot/utils/utils.py b/v2realbot/utils/utils.py index f8c90b3..e75094a 100644 --- a/v2realbot/utils/utils.py +++ b/v2realbot/utils/utils.py @@ -284,6 +284,34 @@ def is_open_rush(dt: datetime, mins: int = 30): rushtime = (datetime.combine(date.today(), business_hours["from"]) + timedelta(minutes=mins)).time() return business_hours["from"] <= dt.time() < rushtime +#TODO market time pro dany den si dotahnout z Alpaca +def is_window_open(dt: datetime, start: int = 0, end: int = 390): + """" + Returns true if time (start in minutes and end in minutes) is in working window + """ + if start < 0 or start > 389: + return False + + if end < 0 or end > 389: + return False + + dt = dt.astimezone(zoneNY) + business_hours = { + "from": time(hour=9, minute=30), + "to": time(hour=16, minute=0) + } + startime = (datetime.combine(date.today(), business_hours["from"]) + timedelta(minutes=start)).time() + endtime = (datetime.combine(date.today(), business_hours["from"]) + timedelta(minutes=end)).time() + + #time not within business hours + if not business_hours["from"] <= dt.time() <= business_hours["to"]: + return False + + if startime <= dt.time() <= endtime: + return True + else: + return False + def is_close_rush(dt: datetime, mins: int = 30): """" Returns true if time is within afternoon rush (close-mins)