From a22aedf978079b7c200e0b9da2242ba246e3d8ba Mon Sep 17 00:00:00 2001 From: David Brazda Date: Mon, 4 Sep 2023 20:10:46 +0200 Subject: [PATCH] frontend config added --- .../common/__pycache__/model.cpython-310.pyc | Bin 7321 -> 7551 bytes v2realbot/common/model.py | 6 + v2realbot/controller/services.py | 112 +++++++++++- v2realbot/main.py | 86 +++++++-- v2realbot/static/index.html | 18 +- v2realbot/static/js/archivechart.js | 9 +- v2realbot/static/js/config.js | 64 +++++++ v2realbot/static/js/configform.js | 166 ++++++++++++++++++ v2realbot/static/js/mytables.js | 2 +- v2realbot/static/js/utils.js | 1 + 10 files changed, 446 insertions(+), 18 deletions(-) create mode 100644 v2realbot/static/js/config.js create mode 100644 v2realbot/static/js/configform.js diff --git a/v2realbot/common/__pycache__/model.cpython-310.pyc b/v2realbot/common/__pycache__/model.cpython-310.pyc index 6329a60f256b3e32fd2617dd6d440034928c3464..6286240d1cc225efb4ddc190875a2bbdf1a26ef3 100644 GIT binary patch delta 2006 zcmZ`)%~M-d6n{5gyrfBpO^AfA1PPR&#X_gnVgxD_q}70cAC>83UU?CNkcam@m@-v@ z*gv41yHQu|IDT~D!s(2gZk%!FM#piwc#bn}-MDkcm3n?BXdwBU9auP`aH4A239Wtp1NP1{3e9#Wxw~mSNtqf z{o_p+8hWJ%8xIL0kNq?y=%?9JhXn0J5EHVW2oh@7_mxPhUjLxzQJ4LFS&0ZG`>TbG zAzK+(g9NA59e<}7WPLySzm7o_CD0s|16Yz%2Z1G#RgVHa7mpRpEa$NNIY2Y25t_K5 z&b9Q35vJN{`69_wEyn7qwN2;c+qBG5L1xv6E>{k7%xQU%cDugombD?r!7u@s1hfJ! z0$u`40a#8m5o0tvtNzt{#n^*PFe)fl&xU@f&SDcED0@4J;T(XuFT;uhUI9!4UIi2Y z%ry_|3}6B98ekEy1Zbw@5>32LkDbvpwSVF%aS2&@@coI?;wq2IJ9(Rqdo(^Ia_U;#InOj4l^@)=1!_m=gVjX2G-=`! zr6>A?$sXqtkNSt`og3b=>@sed;``37dn3(`f1K2I+$H0(}Y!6lyun&j=j%0zy8NA#j zc&O5;vtpOix0T9M6?~I={>-r%KcFqY(o?^{!&FT4$8f;gM?2oDWq&|@-chKP>PO(= zD;8&U{2#+DnswnfMt~E+H|&9TDpJfAayd2C`9rmrY>8{0FKCIYQ{w89xHY(cBv8Cj6?})$u<&2VpM& delta 1885 zcmZ{kOKcle6o!3cXKW{9dty6|okyFOLtJP;jYQhi4NYmoqu{hji&7yl)657QjK|y= zM}(Jw4I6}L?+O;crV?FIRIz5ko&`c6jRX=aV2fB2&VMFNf;7!&z8ufF_q^^ocX#pA zQz<8v>Wk1{{MtX3aU(S*+TT3r2+b<5M=CSwj`&M`oJ*?TqG!~fJ<}CG>Uz@8)az!e z<#?voa9VD?E{kM9>V(@7c?9wiwHSRNH$tMDAoN8NA|diZ4yxVcnEEhwG3NI)EcHw5 z=-LFn|ZuKmJO?^&GiV>6ay_h5_U% z@0-hUXiot1Kv)cOq?lLv#FUuh7_KI66*z{txw+|dTAm+!&9Us7EGi=@pWiQeP_aeY z?Z>V=PE($ShbnLe$N)>gW58K}!wCywfwar&*W`p)xVxK73o)V6{XdHH>i7PfwSujD zK2Jc(0Z#$Tz|+7*fUQ3R=^@}*pa!f0mw+(e=SZ=tHuQ=(hXUOFLO(99aYY{|O--K1 z#tXoc0Q=*R5Y0*u3HxW)`(FLD?6m6Cwx(C#a-G&*6snxQ66Dq8MA#fBw{P=Ma-0%p zvj*P>iLBF4UvsW+*_)oc0^L>MIUp>_%cL;rb&nB(u~2%ZEG~1(tC_8+R>s~L^<8#t z?O+)A6gdDL>M+wAqaTk{au)k-eDB>hn;l!8Ra1pa z)q}J+8J>Gs40@Hb40GC~C10wq3uVz{r+*g~H13b0dU$Ajj;)66>(IVC^tu`WHP?mg zaJ@ep8WUYMffkcoc}zPE8PSiso+&-sg5b2QVN-%{2i(KGEtW4Q0tA4LauAfzpmQHs$cK8f#LOyMrrOX1{p#OI$cVH`KHK^_0 z#F{Fw46rLMBK!r@_!gwMfp>s3aHv%Lo3GVv(tNA_EzOHALMS p?Il6KTx4%V#>sz?ydwg7vxY{Go;T9;WQ`$XnEo3^uTjct{{bP+S0w-d diff --git a/v2realbot/common/model.py b/v2realbot/common/model.py index dec2925..4ce9b1c 100644 --- a/v2realbot/common/model.py +++ b/v2realbot/common/model.py @@ -24,6 +24,12 @@ from alpaca.data.enums import Exchange # return user.id # raise HTTPException(status_code=404, detail=f"Could not find user with id: {id}") +# Define a Pydantic model for input data +class ConfigItem(BaseModel): + id: Optional[int] = None + item_name: str + json_data: str + class Intervals(BaseModel): start: str end: str diff --git a/v2realbot/controller/services.py b/v2realbot/controller/services.py index 4ad56c4..5d0cd8c 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, TestList, Intervals +from v2realbot.common.model import StrategyInstance, Runner, RunRequest, RunArchive, RunArchiveDetail, RunArchiveChange, Bar, TradeEvent, TestList, Intervals, ConfigItem 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 @@ -761,6 +761,7 @@ def get_all_archived_runners_detail(): c = conn.cursor() res = c.execute(f"SELECT data FROM runner_detail") finally: + conn.row_factory = None pool.release_connection(conn) return 0, res.fetchall() @@ -780,6 +781,7 @@ def get_archived_runner_details_byID(id: UUID): result = c.execute(f"SELECT data FROM runner_detail WHERE runner_id='{str(id)}'") res= result.fetchone() finally: + conn.row_factory = None pool.release_connection(conn) if res==None: return -2, "not found" @@ -797,6 +799,114 @@ def insert_archive_detail(archdetail: RunArchiveDetail): pool.release_connection(conn) return res.rowcount +# region TESTLISTS db services +def get_testlists(): + try: + conn = pool.get_connection() + cursor = conn.cursor() + cursor.execute("SELECT id, name, dates FROM test_list") + rows = cursor.fetchall() + finally: + pool.release_connection(conn) + + testlists = [] + for row in rows: + print(row) + testlist = TestList(id=row[0], name=row[1], dates=json.loads(row[2])) + testlists.append(testlist) + + return 0, testlists + +# endregion + +# region CONFIG db services + +def get_all_config_items(): + conn = pool.get_connection() + try: + cursor = conn.cursor() + cursor.execute('SELECT id, item_name, json_data FROM config_table') + config_items = [{"id": row[0], "item_name": row[1], "json_data": row[2]} for row in cursor.fetchall()] + finally: + pool.release_connection(conn) + return 0, config_items + +# Function to get a config item by ID +def get_config_item_by_id(item_id): + conn = pool.get_connection() + try: + cursor = conn.cursor() + cursor.execute('SELECT item_name, json_data FROM config_table WHERE id = ?', (item_id,)) + row = cursor.fetchone() + finally: + pool.release_connection(conn) + if row is None: + return -2, "not found" + else: + return 0, {"item_name": row[0], "json_data": row[1]} + +# Function to get a config item by ID +def get_config_item_by_name(item_name): + #print(item_name) + conn = pool.get_connection() + try: + cursor = conn.cursor() + query = f"SELECT item_name, json_data FROM config_table WHERE item_name = '{item_name}'" + #print(query) + cursor.execute(query) + row = cursor.fetchone() + #print(row) + finally: + pool.release_connection(conn) + if row is None: + return -2, "not found" + else: + return 0, {"item_name": row[0], "json_data": row[1]} + +# Function to create a new config item +def create_config_item(config_item: ConfigItem): + conn = pool.get_connection() + try: + try: + cursor = conn.cursor() + cursor.execute('INSERT INTO config_table (item_name, json_data) VALUES (?, ?)', (config_item.item_name, config_item.json_data)) + item_id = cursor.lastrowid + conn.commit() + print(item_id) + finally: + pool.release_connection(conn) + + return 0, {"id": item_id, "item_name":config_item.item_name, "json_data":config_item.json_data} + except Exception as e: + return -2, str(e) + +# Function to update a config item by ID +def update_config_item(item_id, config_item: ConfigItem): + conn = pool.get_connection() + try: + try: + cursor = conn.cursor() + cursor.execute('UPDATE config_table SET item_name = ?, json_data = ? WHERE id = ?', (config_item.item_name, config_item.json_data, item_id)) + conn.commit() + finally: + pool.release_connection(conn) + return 0, {"id": item_id, **config_item.dict()} + except Exception as e: + return -2, str(e) + +# Function to delete a config item by ID +def delete_config_item(item_id): + conn = pool.get_connection() + try: + cursor = conn.cursor() + cursor.execute('DELETE FROM config_table WHERE id = ?', (item_id,)) + conn.commit() + finally: + pool.release_connection(conn) + return 0, {"id": item_id} + +# endregion + #returns b def get_alpaca_history_bars(symbol: str, datetime_object_from: datetime, datetime_object_to: datetime, timeframe: TimeFrame): """Returns Bar object diff --git a/v2realbot/main.py b/v2realbot/main.py index d728bca..9167505 100644 --- a/v2realbot/main.py +++ b/v2realbot/main.py @@ -14,7 +14,7 @@ import uvicorn from uuid import UUID import v2realbot.controller.services as cs from v2realbot.utils.ilog import get_log_window -from v2realbot.common.model import StrategyInstance, RunnerView, RunRequest, Trade, RunArchive, RunArchiveDetail, Bar, RunArchiveChange, TestList +from v2realbot.common.model import StrategyInstance, RunnerView, RunRequest, Trade, RunArchive, RunArchiveDetail, Bar, RunArchiveChange, TestList, ConfigItem from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException, status, WebSocketException, Cookie, Query from fastapi.responses import HTMLResponse, FileResponse from fastapi.staticfiles import StaticFiles @@ -354,18 +354,11 @@ def create_record(testlist: TestList): # API endpoint to retrieve all records @app.get('/testlists/', dependencies=[Depends(api_key_auth)]) def get_testlists(): - conn = pool.get_connection() - cursor = conn.cursor() - cursor.execute("SELECT id, name, dates FROM test_list") - rows = cursor.fetchall() - pool.release_connection(conn) - - testlists = [] - for row in rows: - testlist = TestList(id=row[0], name=row[1], dates=json.loads(row[2])) - testlists.append(testlist) - - return testlists + res, sada = cs.get_testlists() + if res == 0: + return sada + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found") # API endpoint to retrieve a single record by ID @app.get('/testlists/{record_id}') @@ -416,6 +409,73 @@ def delete_testlist(record_id: str): return {'message': 'Record deleted'} +# region CONFIG APIS + +# Get all config items +@app.get("/config-items/", dependencies=[Depends(api_key_auth)]) +def get_all_items() -> list[ConfigItem]: + res, sada = cs.get_all_config_items() + if res == 0: + return sada + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found") + + +# Get a config item by ID +@app.get("/config-items/{item_id}", dependencies=[Depends(api_key_auth)]) +def get_item(item_id: int)-> ConfigItem: + res, sada = cs.get_config_item_by_id(item_id) + if res == 0: + return sada + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found") + +# Get a config item by Name +@app.get("/config-items-by-name/", dependencies=[Depends(api_key_auth)]) +def get_item(item_name: str)-> ConfigItem: + res, sada = cs.get_config_item_by_name(item_name) + if res == 0: + return sada + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found") + +# Create a new config item +@app.post("/config-items/", dependencies=[Depends(api_key_auth)], status_code=status.HTTP_200_OK) +def create_item(config_item: ConfigItem) -> ConfigItem: + res, sada = cs.create_config_item(config_item) + if res == 0: return sada + else: + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not created: {res}:{id} {sada}") + + +# Update a config item by ID +@app.put("/config-items/{item_id}", dependencies=[Depends(api_key_auth)]) +def update_item(item_id: int, config_item: ConfigItem) -> ConfigItem: + res, sada = cs.get_config_item_by_id(item_id) + if res != 0: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found") + + res, sada = cs.update_config_item(item_id, config_item) + if res == 0: return sada + else: + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not created: {res}:{id}") + + +# Delete a config item by ID +@app.delete("/config-items/{item_id}", dependencies=[Depends(api_key_auth)]) +def delete_item(item_id: int) -> dict: + res, sada = cs.get_config_item_by_id(item_id) + if res != 0: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No data found") + + res, sada = cs.delete_config_item(item_id) + if res == 0: return sada + else: + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"Error not created: {res}:{id}") + +# endregion + + # Thread function to insert data from the queue into the database def insert_queue2db(): print("starting insert_queue2db thread") diff --git a/v2realbot/static/index.html b/v2realbot/static/index.html index 7555b30..ee74251 100644 --- a/v2realbot/static/index.html +++ b/v2realbot/static/index.html @@ -559,13 +559,28 @@

Config

- config options +
+ +

+ + +

+ +
+

+ + + + + +

+ @@ -575,5 +590,6 @@ + \ No newline at end of file diff --git a/v2realbot/static/js/archivechart.js b/v2realbot/static/js/archivechart.js index 2c1f89a..0a13e0a 100644 --- a/v2realbot/static/js/archivechart.js +++ b/v2realbot/static/js/archivechart.js @@ -1,6 +1,9 @@ + var tradeDetails = new Map(); var toolTip = null -var CHART_SHOW_TEXT = false +var CHART_SHOW_TEXT = get_from_config("CHART_SHOW_TEXT", false) + +console.log("CHART_SHOW_TEXT archchart", CHART_SHOW_TEXT) // var vwapSeries = null // var volumeSeries = null var markersLine = null @@ -11,6 +14,7 @@ var slLine = [] //input array object bars = { high: [1,2,3], time: [1,2,3], close: [2,2,2]...} //output array [{ time: 111, open: 11, high: 33, low: 333, close: 333},..] function transform_data(data) { + var SHOW_SL_DIGITS = get_from_config("SHOW_SL_DIGITS", true) transformed = [] //get basic bars, volume and vvwap var bars = [] @@ -64,7 +68,8 @@ function transform_data(data) { sline_markers["position"] = "inBar" sline_markers["color"] = "#f5aa42" //sline_markers["shape"] = "circle" - sline_markers["text"] = histRecord.sl_val.toFixed(3) + console.log("SHOW_SL_DIGITS",SHOW_SL_DIGITS) + sline_markers["text"] = SHOW_SL_DIGITS ? histRecord.sl_val.toFixed(3) : "" sl_line_markers_sada.push(sline_markers) if (index === array.length - 1) { diff --git a/v2realbot/static/js/config.js b/v2realbot/static/js/config.js new file mode 100644 index 0000000..3c23fc7 --- /dev/null +++ b/v2realbot/static/js/config.js @@ -0,0 +1,64 @@ +//JS code for using config value on the frontend +configData = {} + +function get_from_config(name, def_value) { + console.log("required", name, configData) + if ((configData["JS"]) && (configData["JS"][name] !== undefined)) { + console.log("returned from config", configData["JS"][name]) + return configData["JS"][name] + } + else { + console.log("returned def_value", def_value) + return def_value + } +} + +$(document).ready(function () { + const apiBaseUrl = ''; + + // Function to populate the config list and load JSON data initially + function loadConfig(configName) { + const rec = new Object() + rec.item_name = configName + $.ajax({ + url: `${apiBaseUrl}/config-items-by-name/`, + beforeSend: function (xhr) { + xhr.setRequestHeader('X-API-Key', + API_KEY); }, + METHOD: 'GET', + contentType: "application/json", + dataType: "json", + data: rec, + success: function (data) { + console.log(data) + try { + configData[configName] = JSON.parse(data.json_data) + console.log(configData) + console.log("jsme tu") + indConfig = configData["JS"].indConfig + console.log("after") + console.log(JSON.stringify(indConfig, null,null, 2)) + + console.log("before CHART_SHOW_TEXT",CHART_SHOW_TEXT) + var CHART_SHOW_TEXT = configData["JS"].CHART_SHOW_TEXT + console.log("after CHART_SHOW_TEXT",CHART_SHOW_TEXT) + } + catch (error) { + window.alert(`Nešlo rozparsovat JSON_data string ${configName}`, error.message) + } + + }, + error: function(xhr, status, error) { + var err = eval("(" + xhr.responseText + ")"); + window.alert(`Nešlo dotáhnout config nastaveni z db ${configName}`, JSON.stringify(xhr)); + console.log(JSON.stringify(xhr)); + } + }); + + } + + const jsConfigName = "JS" + //naloadovan config + loadConfig(jsConfigName) + +}); diff --git a/v2realbot/static/js/configform.js b/v2realbot/static/js/configform.js new file mode 100644 index 0000000..bdbd0ea --- /dev/null +++ b/v2realbot/static/js/configform.js @@ -0,0 +1,166 @@ +//JS code for FRONTEND CONFIG FORM +$(document).ready(function () { + // API Base URL + const apiBaseUrl = ''; + let editingItemId = null; + var localArray = [] + + + // Function to populate the config list and load JSON data initially + function populateConfigList(to_select = null) { + $.ajax({ + url: `${apiBaseUrl}/config-items/`, + beforeSend: function (xhr) { + xhr.setRequestHeader('X-API-Key', + API_KEY); }, + type: 'GET', + success: function (data) { + const configList = $('#configList'); + configList.empty(); + + localArray = data + selected = "" + data.forEach((item, index, array) => { + selected = ""; + //pokud prijde id ktere mame vybrat vybereme to, jinak vybereme prvni + if (((to_select !== null) && (to_select == item.id)) || ((to_select == null) && (index==0))) { + selected = "SELECTED" + $('#itemName').val(item.item_name); + $('#jsonTextarea').val(item.json_data); + editingItemId = item.id; + } + configList.append(``); + }); + + } + }); + } + + // + function showJSONdata(itemId) { + localArray.forEach((item, index, array) => { + if (item.id == itemId) { + $('#itemName').val(item.item_name); + $('#jsonTextarea').val(item.json_data); + editingItemId = itemId; + } + }); + } + + $('#cancelButton').attr('disabled', true); + // Populate the config list and load JSON data and item name initially + populateConfigList(); + + // Event listener for config list change + $('#configList').change(function () { + const selectedItem = $(this).val(); + console.log(selectedItem) + if (selectedItem) { + showJSONdata(selectedItem); + } + }); + + // Save or add a config item + $('#saveButton').click(function () { + const itemName = $('#itemName').val(); + const jsonData = $('#jsonTextarea').val(); + var validformat = false + $('#addButton').attr('disabled', false); + $('#deleteButton').attr('disabled', false); + + try { + var parsedJSON = JSON.parse(jsonData) + validformat = true + } + catch (error) { + alert("Not valid JSON", error.message) + } + + if (validformat) { + var confirmed = window.confirm("Sure?"); + + if (editingItemId && confirmed) { + // Update the selected item with the modified data using API + $.ajax({ + url: `${apiBaseUrl}/config-items/${editingItemId}`, + beforeSend: function (xhr) { + xhr.setRequestHeader('X-API-Key', + API_KEY); }, + type: 'PUT', + contentType: 'application/json', + data: JSON.stringify({ item_name: itemName, json_data: jsonData }), + success: function () { + alert('Data saved successfully.'); + populateConfigList(editingItemId); // Refresh the config list + } + }); + + } else if (confirmed) { + // Add a new config item using API + $.ajax({ + url: `${apiBaseUrl}/config-items/`, + beforeSend: function (xhr) { + xhr.setRequestHeader('X-API-Key', + API_KEY); }, + type: "POST", + contentType: "application/json", + dataType: "json", + data: JSON.stringify({ item_name: itemName, json_data: jsonData }), + success: function (data) { + console.log(data) + alert('New item added successfully.'); + populateConfigList(data.id); // Refresh the config list + } + }); + } + } + }); + + // Add a new config item (populates a new record in the form) + $('#addButton').click(function () { + $('#configList').val(''); + $('#itemName').val(''); + $('#jsonTextarea').val(''); + editingItemId = null; + $('#addButton').attr('disabled', true); + $('#deleteButton').attr('disabled', true); + $('#cancelButton').attr('disabled', false); + + }); + + // Add a new config item (populates a new record in the form) + $('#cancelButton').click(function () { + $('#addButton').attr('disabled', false); + $('#deleteButton').attr('disabled', false); + $('#cancelButton').attr('disabled', true); + populateConfigList(); + }); + + // Delete a config item + $('#deleteButton').click(function () { + if (editingItemId == null) { + $('#configList').val(''); + $('#itemName').val(''); + $('#jsonTextarea').val(''); + } + else { + var confirmed = window.confirm("Confirm?"); + if (confirmed) { + + $.ajax({ + url: `${apiBaseUrl}/config-items/${editingItemId}`, + beforeSend: function (xhr) { + xhr.setRequestHeader('X-API-Key', + API_KEY); }, + type: 'DELETE', + success: function () { + alert('Item deleted successfully.'); + populateConfigList(); // Refresh the config list + } + }); + } + } + + }); + +}); \ No newline at end of file diff --git a/v2realbot/static/js/mytables.js b/v2realbot/static/js/mytables.js index cd9f8b1..d67f098 100644 --- a/v2realbot/static/js/mytables.js +++ b/v2realbot/static/js/mytables.js @@ -525,7 +525,7 @@ var stratinRecords = {data: 'history', visible: false}, {data: 'id', visible: true}, ], - paging: false, + paging: true, processing: false, columnDefs: [{ targets: 12, diff --git a/v2realbot/static/js/utils.js b/v2realbot/static/js/utils.js index 5e468a0..1f9882e 100644 --- a/v2realbot/static/js/utils.js +++ b/v2realbot/static/js/utils.js @@ -50,6 +50,7 @@ indConfig = [ {name: "ema", titlevisible: false, embed: true, display: true, pri {name: "ppo", titlevisible: true, embed: true, display: true, priceScaleId: "middle", lastValueVisible: false}, {name: "stoch2", titlevisible: true, embed: true, display: true, priceScaleId: "middle", lastValueVisible: false}, {name: "sec_price", titlevisible: true, embed: true, display: true, priceScaleId: "right", lastValueVisible: false},] +console.log(JSON.stringify(indConfig, null,null, 2)) function initialize_statusheader() {