From 5dc06afe67236c4d6fe98ffc014c12cdc10f56ef Mon Sep 17 00:00:00 2001 From: Mike Kipnis Date: Sat, 12 Sep 2026 09:12:15 -0400 Subject: [PATCH] DataService: front-end for sqlite --- DataService/sql/FrontEnd/README.md | 6 + DataService/sql/FrontEnd/assets/custom.css | 156 +++++++ .../components/instrument_details_panel.py | 78 ++++ .../FrontEnd/components/instruments_panel.py | 237 +++++++++++ .../sql/FrontEnd/components/markets_panel.py | 177 ++++++++ .../components/user_group_markets_panel.py | 135 ++++++ .../FrontEnd/components/user_group_panel.py | 208 ++++++++++ .../sql/FrontEnd/components/user_panel.py | 234 +++++++++++ .../sql/FrontEnd/data_service_front_end.py | 383 ++++++++++++++++++ DataService/sql/FrontEnd/db_connection.py | 138 +++++++ DataService/sql/FrontEnd/requirements.in | 5 + DataService/sql/FrontEnd/requirements.txt | 101 +++++ .../cpp_ws_reactjs/dash_trader/dash_trader.py | 189 +++++++++ MiscClients/dash_ws/README.md | 9 + MiscClients/dash_ws/components/login_panel.py | 185 +++++++++ .../components/positions_and_market_data.py | 57 +++ .../dash_ws/components/price_level_panel.py | 59 +++ .../dash_ws/components/ticket_panel.py | 24 ++ MiscClients/dash_ws/dash_trader.py | 208 ++++++++++ MiscClients/dash_ws/requirements.txt | 5 + MiscClients/dash_ws/ws_server.py | 68 ++++ 21 files changed, 2662 insertions(+) create mode 100644 DataService/sql/FrontEnd/README.md create mode 100644 DataService/sql/FrontEnd/assets/custom.css create mode 100644 DataService/sql/FrontEnd/components/instrument_details_panel.py create mode 100644 DataService/sql/FrontEnd/components/instruments_panel.py create mode 100644 DataService/sql/FrontEnd/components/markets_panel.py create mode 100644 DataService/sql/FrontEnd/components/user_group_markets_panel.py create mode 100644 DataService/sql/FrontEnd/components/user_group_panel.py create mode 100644 DataService/sql/FrontEnd/components/user_panel.py create mode 100644 DataService/sql/FrontEnd/data_service_front_end.py create mode 100644 DataService/sql/FrontEnd/db_connection.py create mode 100644 DataService/sql/FrontEnd/requirements.in create mode 100644 DataService/sql/FrontEnd/requirements.txt create mode 100644 MiscClients/cpp_ws_reactjs/dash_trader/dash_trader.py create mode 100644 MiscClients/dash_ws/README.md create mode 100644 MiscClients/dash_ws/components/login_panel.py create mode 100644 MiscClients/dash_ws/components/positions_and_market_data.py create mode 100644 MiscClients/dash_ws/components/price_level_panel.py create mode 100644 MiscClients/dash_ws/components/ticket_panel.py create mode 100644 MiscClients/dash_ws/dash_trader.py create mode 100644 MiscClients/dash_ws/requirements.txt create mode 100644 MiscClients/dash_ws/ws_server.py diff --git a/DataService/sql/FrontEnd/README.md b/DataService/sql/FrontEnd/README.md new file mode 100644 index 00000000..22adfc24 --- /dev/null +++ b/DataService/sql/FrontEnd/README.md @@ -0,0 +1,6 @@ +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip setuptools wheel +pip install -r requirements.txt + +gunicorn DataServiceFrontEnd:server --bind 0.0.0.0:8050 diff --git a/DataService/sql/FrontEnd/assets/custom.css b/DataService/sql/FrontEnd/assets/custom.css new file mode 100644 index 00000000..b9977e46 --- /dev/null +++ b/DataService/sql/FrontEnd/assets/custom.css @@ -0,0 +1,156 @@ +body { + font-size: 14px; +} + +.card { + border-radius: 2px !important; +} + +.form-control { + border-radius: 2px !important; + font-size: 14px !important; + padding: 2px 6px !important; + min-height: 26px !important; +} + +/* Accordion container */ +.accordion { + background-color: #0e2537 !important; /* dark container */ +} + +/* Each panel item */ +.accordion-item { + background-color: #0e2537 !important; + border: 0px solid #2a2f42 !important; +} + +/* Header / button */ +.accordion-button { + background-color: #0e2537 !important; + color: #f09d08 !important; + border: 0px solid #2a2f42 !important; +} + +/* Expanded header */ +.accordion-button:not(.collapsed) { + background-color: #0f1118 !important; + color: white !important; +} + +/* Hover */ +.accordion-button:hover { + color: white !important; +} + +/* Body content */ +.accordion-body { + background-color: #0e2537 !important; + color: #cccccc !important; + padding: 2px !important; +} + +/* Arrow icon */ +.accordion-button::after { + filter: invert(65%) !important; +} + +/* Remove focus outline */ +.accordion-button:focus { + outline: none !important; + box-shadow: none !important; +} + +/* ========================================================= + Global Buttons + ========================================================= */ + +button { + background-color: #343a40; + color: white; + border: 1px solid #555; + border-radius: 4px; + padding: 6px 16px; + cursor: pointer; +} + +button:hover { + background-color: #495057; +} + +button:active { + background-color: #212529; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + + +/* ========================================================= + Global Inputs + ========================================================= */ + +input, +textarea, +select { + background-color: #343a40 !important; + color: white !important; + border: 1px solid #555 !important; + border-radius: 4px; +} + +input::placeholder, +textarea::placeholder { + color: #adb5bd !important; + opacity: 1; +} + +input:focus, +textarea:focus, +select:focus { + background-color: #343a40 !important; + color: white !important; + border-color: #777 !important; + box-shadow: none !important; + outline: none; +} + + +/* ========================================================= + Labels + ========================================================= */ + +label { + color: white; +} + + +/* ========================================================= + Common layout helpers + ========================================================= */ + +.input-container { + margin-top: 10px; +} + +.button-container { + display: flex; + justify-content: flex-end; + margin-top: 8px; +} + +.error-message { + color: #dc3545; + margin-top: 5px; +} + +.ag-theme-quartz { + --ag-font-size: 14px; + --ag-row-height: 30px; + --ag-header-height: 32px; +} + +.ag-root-wrapper { + border-radius: 0 !important; +} \ No newline at end of file diff --git a/DataService/sql/FrontEnd/components/instrument_details_panel.py b/DataService/sql/FrontEnd/components/instrument_details_panel.py new file mode 100644 index 00000000..745f173a --- /dev/null +++ b/DataService/sql/FrontEnd/components/instrument_details_panel.py @@ -0,0 +1,78 @@ +# Copyright (c) Mike Kipnis - DistributedATS +import json +import logging +import dash_ag_grid as dag +from dash import html, dash, Output, Input + +import db_connection + +logger = logging.getLogger("InstrumentDetailsPanel") + +class InstrumentDetailsPanel(object): + + def __init__(self, app: dash.Dash): + + self.app = app + + column_defs = [ + {"field": "key", "headerName": "Key", "flex": 2}, + {"field": "value", "headerName": "value", "flex": 2}, + ] + + self.instrument_details_grid = dag.AgGrid( + id="instrument_details_grid_id", + columnDefs=column_defs, + rowData=[], + defaultColDef={"minWidth": 100, "resizable": True}, + style={"height": "400px", "width": "100%"}, + className="ag-theme-quartz", + dashGridOptions={ + "rowSelection": "single", + "animateRows": True, + "suppressMaintainUnsortedOrder": True + }, + ) + + self._register_callbacks() + + pass + + def layout(self): + return html.Div( + children=[self.instrument_details_grid], + **{"data-ag-theme-mode": "dark"} # Unpacks the dictionary as an HTML keyword attribute + ) + + def _register_callbacks(self): + @self.app.callback( + Output("instrument_details_grid_id", "rowData"), + Input("database-sqlite", "children"), + Input("selected-instrument", "data"), + prevent_initial_call=True, + ) + def select_instrument(database_sqlite, selected_instrument): + + if not selected_instrument: + return [] + + logger.info(f"select_instrument: {selected_instrument}") + + instrument = selected_instrument[0] + + instruments = db_connection.get_details_for_instrument(database_sqlite, instrument['instrument_name']) + + if len(instruments) == 0: + return [] + + instrument_data_with_properties = instruments[0] + instrument_properties = json.loads(instrument_data_with_properties.get('properties',{})) + ref_data = {} + if 'ref_data' in instrument_properties: + ref_data = json.loads(instrument_properties.get('ref_data',{})) + + ref_data_out = [] + for key, value in ref_data.items(): + ref_data_out.append({'key': key, 'value': value}) + + + return ref_data_out diff --git a/DataService/sql/FrontEnd/components/instruments_panel.py b/DataService/sql/FrontEnd/components/instruments_panel.py new file mode 100644 index 00000000..45bcf8c8 --- /dev/null +++ b/DataService/sql/FrontEnd/components/instruments_panel.py @@ -0,0 +1,237 @@ +# Copyright (c) Mike Kipnis - DistributedATS +import json +import logging +import dash_ag_grid as dag +from dash import html, dash, Output, Input, State +import dash_bootstrap_components as dbc + +import db_connection + +logger = logging.getLogger("InstrumentsPanel") + +class InstrumentsPanel(object): + + def __init__(self, app: dash.Dash): + + self.app = app + + column_defs = [ + {"field": "instrument_name", "headerName": "Instrument Name", "flex": 2}, + {"field": "last_update_time", "headerName": "Last Update Time", "flex": 2}, + ] + + self.instrument_grid = dag.AgGrid( + id="instrument_grid_id", + columnDefs=column_defs, + rowData=[], + defaultColDef={"minWidth": 100, "resizable": True}, + style={"height": "400px", "width": "100%"}, + className="ag-theme-quartz", + dashGridOptions={ + "rowSelection": "single", + "animateRows": True, + "suppressMaintainUnsortedOrder": True + }, + ) + + self._register_callbacks() + + pass + + def layout(self): + return html.Div( + children=[ + self.instrument_grid, + + html.Div( + children=[ + dbc.Row( + [ + dbc.Col( + [ + html.Label("Instrument Name", + style={"fontWeight": "bold", "marginBottom": "4px", }, ), + dbc.Input(id="instrument_name_input_id", type="text", + placeholder="Enter Instrument Name", + size="sm", style={"width": "300px", }, ), + ], + width="auto", + ), + + dbc.Col( + [ + html.Label( + "Instrument Type", + style={"fontWeight": "bold", "marginBottom": "4px"}, + ), + dbc.Select( + id="instrument_type_select_id", + options=[ + {"label": "Equity", "value": "equity"}, + {"label": "UST", "value": "UST"}, + {"label": "Crypto", "value": "crypto"}, + ], + value="Equity", + placeholder="Instrument Type", + size="sm", + style={"width": "300px"}, + ), + ], + width="auto", + ), + + dbc.Col( + [ + html.Label( + "Tick Size", + style={"fontWeight": "bold", "marginBottom": "4px"}, + ), + dbc.Select( + id="tick_size_select_id", + options=[ + {"label": "100", "value": "100"}, + {"label": "256", "value": "256"}, + ], + value="100", + placeholder="Tick Size Select", + size="sm", + style={"width": "300px"}, + ), + ], + width="auto", + ), + + dbc.Col( + [ + html.Button("Add", id="instrument_add", className="btn btn-dark btn-sm me-2", ), + html.Button("Remove", id="instrument_remove", className="btn btn-dark btn-sm", ), + ], + width="auto", className="d-flex align-items-end", + ), + ], + className="align-items-end", + ), + ], + ), + + html.Div(id="instrument_panel_error", style={"color": "#dc3545", "marginTop": "5px", }, ), + + ], + **{"data-ag-theme-mode": "dark"} # Unpacks the dictionary as an HTML keyword attribute + ) + + def _register_callbacks(self): + @self.app.callback( + Output("instrument_grid_id", "rowData"), + Input("database-sqlite", "children"), + Input("selected-market", "data"), + prevent_initial_call=True, + ) + def select_instrument(database_sqlite, selected_market): + + if not selected_market: + return [] + + instrument = db_connection.get_instruments_for_market(database_sqlite, selected_market['market_name']) + + return instrument + + @self.app.callback( + Output("instrument_grid_id", "selectedRows"), + Input("instrument_grid_id", "rowData"), + prevent_initial_call=True, + ) + def select_first_row(row_data): + + if not row_data: + return [] + + return [row_data[0]] + + @self.app.callback( + Output("selected-instrument", "data"), + Output("instrument_name_input_id", "value"), + Output("instrument_type_select_id", "value"), + Output("tick_size_select_id", "value"), + Input("database-sqlite", "children"), + Input("instrument_grid_id", "selectedRows"), + prevent_initial_call=True, + ) + def select_first_row(database_sqlite, row_data): + + if not row_data: + return [],'','','' + + selected_instrument = row_data[0] + instrument_details = db_connection.get_details_for_instrument(database_sqlite, selected_instrument['instrument_name']) + + type_instrument_type = 'equity' + tick_size = 100 + if len(instrument_details) > 0: + properties = json.loads(instrument_details[0]['properties']) + type_instrument_type = properties.get('type', type_instrument_type) + tick_size = properties.get('tick_size', tick_size) + + return [selected_instrument], selected_instrument['instrument_name'], type_instrument_type, tick_size + + + @self.app.callback( + Output("instrument_name_input_id", "value", allow_duplicate=True, ), + Output("instrument_panel_error", "children", ), + Output("selected-market", "data", allow_duplicate=True,), + Input("instrument_add", "n_clicks", ), + State("instrument_name_input_id", "value", ), + State("selected-market", "data"), + State("instrument_type_select_id", "value", ), + State("tick_size_select_id", "value", ), + State("database-sqlite", "children"), + prevent_initial_call=True, + ) + def add_instrument(add_clicks, instrument_name, selected_market, instrument_type_select_id, tick_size_select_id, database_sqlite): + + instrument_name = (instrument_name or "").strip().upper() + + if not instrument_name: + return instrument_name, "Instrument Name is required.", selected_market + + logger.info(f"add_instrument: {instrument_name}") + + current_instrument_details = db_connection.get_details_for_instrument(database_sqlite, instrument_name) + + properties = {} + if len(current_instrument_details) > 0: + properties = json.loads(current_instrument_details[0].get('properties','')) + + properties['type'] = instrument_type_select_id + properties['tick_size'] = tick_size_select_id + + db_connection.ins_details_for_instrument(database_sqlite, instrument_name, properties) + db_connection.insert_instrument_into_market(database_sqlite, instrument_name, selected_market['market_name']) + + return instrument_name, "", selected_market + + + @self.app.callback( + Output("instrument_name_input_id", "value", allow_duplicate=True, ), + Output("instrument_panel_error", "children", allow_duplicate=True,), + Output("selected-market", "data", allow_duplicate=True, ), + Input("instrument_remove", "n_clicks", ), + State("instrument_name_input_id", "value", ), + State("selected-market", "data"), + State("database-sqlite", "children"), + prevent_initial_call=True, + ) + def delete_instrument(remove_clicks, instrument_name, selected_market, database_sqlite): + + instrument_name = (instrument_name or "").strip().upper() + + logger.info(f"delete_instrument: {instrument_name}") + + if not instrument_name: + return instrument_name, "Instrument Name is required.", selected_market + + db_connection.delete_instrument_from_market(database_sqlite, instrument_name, selected_market['market_name']) + + return instrument_name, "", selected_market + + diff --git a/DataService/sql/FrontEnd/components/markets_panel.py b/DataService/sql/FrontEnd/components/markets_panel.py new file mode 100644 index 00000000..80c65665 --- /dev/null +++ b/DataService/sql/FrontEnd/components/markets_panel.py @@ -0,0 +1,177 @@ +# Copyright (c) Mike Kipnis - DistributedATS +import logging +import dash_ag_grid as dag +from dash import html, dash, Output, Input, State +import dash_bootstrap_components as dbc + +import db_connection + +logger = logging.getLogger("MarketsPanel") + +class MarketsPanel(object): + + def __init__(self, app: dash.Dash): + + self.app = app + + column_defs = [ + {"field": "market_name", "headerName": "Market", "flex": 2}, + {"field": "last_update_time", "headerName": "Last Update Time", "flex": 2}, + ] + + self.markets_grid = dag.AgGrid( + id="markets_grid_id", + columnDefs=column_defs, + rowData=[], + defaultColDef={"minWidth": 100, "resizable": True}, + style={"height": "400px", "width": "100%"}, + className="ag-theme-quartz", + dashGridOptions={ + "rowSelection": "single", + "animateRows": True, + "suppressMaintainUnsortedOrder": True + }, + ) + + self._register_callbacks() + + pass + + def layout(self): + return html.Div( + children=[ + self.markets_grid, + + # ------------------------------------------------- + # Input fields + # ------------------------------------------------- + html.Div( + children=[ + dbc.Row( + [ + dbc.Col( + [ + html.Label("Market", style={"fontWeight": "bold", "marginBottom": "4px", }, ), + dbc.Input(id="market_input", type="text", placeholder="Enter market", size="sm", style={"width": "300px", }, ), + ], + width="auto", + ), + + dbc.Col( + [ + html.Button("Add", id="market_add", className="btn btn-dark btn-sm me-2", ), + html.Button("Remove", id="market_remove",className="btn btn-dark btn-sm", ), + ], + width="auto", className="d-flex align-items-end", + ), + ], + className="align-items-end", + ), + ], + ), + + html.Div( id="market_error", style={ "color": "#dc3545", "marginTop": "5px", },), + ], + **{"data-ag-theme-mode": "dark"} # Unpacks the dictionary as an HTML keyword attribute + ) + + def _register_callbacks(self): + @self.app.callback( + Output("markets_grid_id", "rowData"), + Input("markets", "data"), + prevent_initial_call=True, + ) + def setup_entity_grid(markets): + + if not markets: + return [] + + return markets + + @self.app.callback( + Output("markets_grid_id", "selectedRows"), + Input("markets_grid_id", "rowData"), + prevent_initial_call=True, + ) + def select_first_row(row_data): + + if not row_data: + return [] + + return [row_data[0]] + + @self.app.callback( + Output("selected-market", "data"), + Output("market_input", "value", allow_duplicate=True,), + Input("markets_grid_id", "selectedRows"), + prevent_initial_call=True, + ) + def select_market(row_data): + + if not row_data: + return [], '' + + selected_market = row_data[0] + + return selected_market, selected_market['market_name'] + + @self.app.callback( + Output("market_input", "value", ), + Output("market_error", "children", ), + Output("database-sqlite", "children", allow_duplicate=True,), + Input("market_add", "n_clicks", ), + State("market_input", "value", ), + State("markets_grid_id", "rowData", ), + State("database-sqlite", "children"), + prevent_initial_call=True, + ) + def add_market(add_clicks, market_name, row_data, database_sqlite): + + market_name = (market_name or "").strip().upper() + + if not market_name: + return market_name, "Market Name is required.", database_sqlite + + if any(row.get("market_name") == market_name for row in row_data): + return market_name, f"Market already exists. {market_name}", database_sqlite + + db_connection.insert_market(database_sqlite, market_name) + + market_matching_engine_user = 'MATCHING_ENGINE_' + market_name + market_matching_engine_user_props = {'name':'Matching Market ' + market_name, 'type':'MATCHING_ENGINE'} + + market_matching_engine_user_group = 'MATCHING_ENGINE_' + market_name + '_USER_GROUP' + + db_connection.insert_user_group(database_sqlite, market_matching_engine_user_group) + db_connection.insert_user(database_sqlite, market_matching_engine_user, + market_matching_engine_user_group, market_matching_engine_user_props) + db_connection.set_market_for_user_group(database_sqlite, market_matching_engine_user_group, market_name) + + logger.info(f"add_market: {market_matching_engine_user} - {market_matching_engine_user_props} - {market_matching_engine_user_group}") + + return "", "", database_sqlite + + @self.app.callback( + Output("market_error", "children", allow_duplicate=True, ), + Output("database-sqlite", "children", allow_duplicate=True, ), + Input("market_remove", "n_clicks", ), + State("market_input", "value", ), + State("database-sqlite", "children"), + prevent_initial_call=True, + ) + def delete_market(remove_clicks, market_name, database_sqlite): + + market_name = (market_name or "").strip().upper() + + logger.info(f"delete_market: {market_name}") + + if not market_name: + return "Market Name is required.", database_sqlite + + db_connection.delete_market(database_sqlite, market_name) + + logger.info(f"delete_market: {market_name}") + + return "", database_sqlite + + diff --git a/DataService/sql/FrontEnd/components/user_group_markets_panel.py b/DataService/sql/FrontEnd/components/user_group_markets_panel.py new file mode 100644 index 00000000..84ecc67a --- /dev/null +++ b/DataService/sql/FrontEnd/components/user_group_markets_panel.py @@ -0,0 +1,135 @@ +# Copyright (c) Mike Kipnis - DistributedATS +import logging +import dash_ag_grid as dag +from dash import html, dash, Output, Input, State +import dash_bootstrap_components as dbc + +import db_connection + +logger = logging.getLogger("UserGroupMarketsPanel") + +class UserGroupMarketsPanel(object): + + def __init__(self, app: dash.Dash): + + self.app = app + + column_defs = [ + {"field": "market_name", "headerName": "Markets", "flex": 2}, + ] + + self.user_group_markets_grid = dag.AgGrid( + id="user_group_markets_grid_id", + columnDefs=column_defs, + rowData=[], + defaultColDef={"minWidth": 100, "resizable": True}, + style={"height": "400px", "width": "100%"}, + className="ag-theme-quartz", + dashGridOptions={ + "rowSelection": "single", + "animateRows": True, + "suppressMaintainUnsortedOrder": True + }, + ) + + self._register_callbacks() + + pass + + def layout(self): + return html.Div( + children=[ + self.user_group_markets_grid, + dbc.Col( + [ + html.Label( + "Market Mapping", + style={"fontWeight": "bold", "marginBottom": "4px"}, + ), + dbc.Select( + id="market_select", + options=[], + placeholder="Select Market", + size="sm", + style={"width": "300px"}, + ), + ], + width="auto", + ), + + dbc.Col( + [ + html.Button("Add", id="market_add_to_group", className="btn btn-dark btn-sm me-2", ), + html.Button("Remove", id="market_remove_from_group", className="btn btn-dark btn-sm", ), + ], + width="auto", + className="d-flex align-items-end", + ), + ], + **{"data-ag-theme-mode": "dark"} # Unpacks the dictionary as an HTML keyword attribute + ) + + def _register_callbacks(self): + @self.app.callback( + Output("user_group_markets_grid_id", "rowData"), + Input("database-sqlite", "children"), + Input("selected-group", "data"), + prevent_initial_call=True, + ) + def select_markets_for_user_group(database_sqlite, selected_group): + + if not selected_group: + return [] + + user_group = selected_group[0] + users = db_connection.get_markets_for_user_group(database_sqlite, user_group['user_group']) + + return users + + @self.app.callback( + Output("market_select", "options"), + Input("markets", "data"), + prevent_initial_call=True, + ) + def setup_entity_grid(markets): + + if not markets: + return [] + + market_dropdown = [] + + for market in markets: + market_dropdown_item = {} + market_dropdown_item['label'] = market['market_name'] + market_dropdown_item['value'] = market['market_name'] + market_dropdown.append(market_dropdown_item) + + return market_dropdown + + + @self.app.callback( + Output("selected-group", "data", allow_duplicate=True, ), + Input("market_add_to_group", "n_clicks", ), + State("selected-group", "data"), + State("market_select", "value", ), + State("database-sqlite", "children"), + prevent_initial_call=True, + ) + def add_market_to_group(market_add_to_group, selected_group, market_select, database_sqlite): + logger.info(f"add_market_to_group: {selected_group} {market_select}") + db_connection.set_market_for_user_group(database_sqlite, selected_group[0]['user_group'], market_select) + return selected_group + + @self.app.callback( + Output("selected-group", "data", allow_duplicate=True, ), + Input("market_remove_from_group", "n_clicks", ), + State("selected-group", "data"), + State("user_group_markets_grid_id","selectedRows", ), + State("database-sqlite", "children"), + prevent_initial_call=True, + ) + def remove_market_from_group(market_remove_from_group, selected_group, market_select, database_sqlite): + logger.info(f"remove_market_from_group: {selected_group} {market_select}") + db_connection.remove_market_for_user_group(database_sqlite, selected_group[0]['user_group'], market_select[0]['market_name']) + return selected_group + diff --git a/DataService/sql/FrontEnd/components/user_group_panel.py b/DataService/sql/FrontEnd/components/user_group_panel.py new file mode 100644 index 00000000..9e3163ce --- /dev/null +++ b/DataService/sql/FrontEnd/components/user_group_panel.py @@ -0,0 +1,208 @@ +# Copyright (c) Mike Kipnis - DistributedATS +import logging +from datetime import datetime + +import dash_ag_grid as dag +from dash import html, Input, Output, State, dash, ctx +import dash_bootstrap_components as dbc + +import db_connection + +logger = logging.getLogger("UserGroupPanel") + + +class UserGroupPanel(object): + + def __init__(self, app: dash.Dash): + + self.app = app + + column_defs = [ + { + "field": "user_group", + "headerName": "User Group", + "flex": 2, + "editable": True, + }, + { + "field": "last_update_time", + "headerName": "Last Update Time", + "flex": 2, + "editable": False, + }, + ] + + self.user_group_grid = dag.AgGrid( + id="user_group_grid_id", + columnDefs=column_defs, + rowData=[], + + defaultColDef={ + "minWidth": 100, + "resizable": True, + }, + + style={ + "height": "400px", + "width": "100%", + }, + + dashGridOptions={ + "rowSelection": "single", + "animateRows": True, + "suppressMaintainUnsortedOrder": True, + "doubleClickEdit": True, + }, + className="ag-theme-quartz", + ) + + self._register_callbacks() + + def layout(self): + + return html.Div( + children=[ + # ------------------------------------------------- + # Grid + # ------------------------------------------------- + + self.user_group_grid, + + # ------------------------------------------------- + # Input fields + # ------------------------------------------------- + html.Div( + children=[ + dbc.Row( + [ + dbc.Col( + [ + html.Label( "User Group", style={ "fontWeight": "bold", "marginBottom": "4px", }, ), + dbc.Input( id="user_group_input", type="text", placeholder="Enter user group", size="sm", style={ "width": "300px", }, ), + ], + width="auto", + ), + + dbc.Col( + [ + html.Button( "Add", id="user_group_add", className="btn btn-dark btn-sm me-2", ), + html.Button( "Remove", id="user_group_remove", className="btn btn-dark btn-sm", ), + ], + width="auto", + className="d-flex align-items-end", + ), + ], + className="align-items-end", + ), + ], + ), + + # ------------------------------------------------- + # Error message + # ------------------------------------------------- + html.Div(id="user_group_error", style={ "color": "#dc3545", "marginTop": "5px", },), + ], + + **{ + "data-ag-theme-mode": "dark" + } + ) + + def _register_callbacks(self): + + # --------------------------------------------------------- + # Load initial data + # --------------------------------------------------------- + + @self.app.callback( + Output("user_group_grid_id", "rowData"), + Input("user-groups", "data"), + prevent_initial_call=True, + ) + def setup_entity_grid(user_groups): + + if not user_groups: + return [] + + return user_groups + + # --------------------------------------------------------- + # Add row + # --------------------------------------------------------- + @self.app.callback( + Output("user_group_error", "children", allow_duplicate=True, ), + Output("database-sqlite", "children", allow_duplicate=True, ), + Input("user_group_add","n_clicks",), + State("user_group_input", "value", ), + State("database-sqlite", "children"), + prevent_initial_call=True, + ) + def add_user_group(add_clicks,user_group,database_sqlite): + + user_group = (user_group or "").strip() + + logger.info(f"add_user_group: {user_group}") + + if not user_group: + return "User Group is required.", database_sqlite + + db_connection.insert_user_group(database_sqlite, user_group) + + return "", database_sqlite + + # --------------------------------------------------------- + # Remove row + # --------------------------------------------------------- + + @self.app.callback( + Output("user_group_error", "children", allow_duplicate=True, ), + Output("database-sqlite", "children", allow_duplicate=True, ), + Input("user_group_remove","n_clicks",), + State("user_group_input", "value", ), + State("database-sqlite", "children"), + prevent_initial_call=True, + ) + def remove_row(remove_clicks,user_group, database_sqlite,): + + user_group = (user_group or "").strip().upper() + + logger.info(f"remove_user_group: {user_group}") + + if not user_group: + return "User Group Name is required.", database_sqlite + + db_connection.remove_user_group(database_sqlite, user_group) + + return "", database_sqlite + + # --------------------------------------------------------- + # Automatically select first row + # --------------------------------------------------------- + + @self.app.callback( + Output("user_group_grid_id","selectedRows",), + Input("user_group_grid_id","rowData",), + prevent_initial_call=True, + ) + def select_first_row(row_data): + + if not row_data: + return [] + + return [row_data[0]] + + # --------------------------------------------------------- + # Publish selected group + # --------------------------------------------------------- + @self.app.callback( + Output("selected-group","data",), + Output("user_group_input", "value", allow_duplicate=True, ), + Input("user_group_grid_id","selectedRows",), + prevent_initial_call=True, + ) + def select_group(selected_rows): + + if not selected_rows: + return [] + + return [selected_rows[0]], selected_rows[0]['user_group'] \ No newline at end of file diff --git a/DataService/sql/FrontEnd/components/user_panel.py b/DataService/sql/FrontEnd/components/user_panel.py new file mode 100644 index 00000000..26c05640 --- /dev/null +++ b/DataService/sql/FrontEnd/components/user_panel.py @@ -0,0 +1,234 @@ +# Copyright (c) Mike Kipnis - DistributedATS +import json +import logging +import dash_ag_grid as dag +from dash import html, dash, Output, Input, State +from numpy.ma.core import append +import dash_bootstrap_components as dbc + +import db_connection + +logger = logging.getLogger("UsersPanel") + +class UsersPanel(object): + + def __init__(self, app: dash.Dash): + + self.app = app + + column_defs = [ + {"field": "user_name", "headerName": "User Group", "flex": 2}, + {"field": "name", "headerName": "Name", "flex": 2}, + {"field": "type", "headerName": "Type", "flex": 2}, + {"field": "last_update_time", "headerName": "Last Update Time", "flex": 2}, + ] + + self.users_grid = dag.AgGrid( + id="users_grid_id", + columnDefs=column_defs, + rowData=[], + defaultColDef={"minWidth": 100, "resizable": True}, + style={"height": "400px", "width": "100%"}, + className="ag-theme-quartz", + dashGridOptions={ + "rowSelection": "single", + "animateRows": True, + "suppressMaintainUnsortedOrder": True + }, + ) + + self._register_callbacks() + + pass + + def layout(self): + return html.Div( + children=[ + self.users_grid, + html.Div( + children=[ + dbc.Row( + [ + dbc.Col( + [ + html.Label("User Name", + style={"fontWeight": "bold", "marginBottom": "4px", }, ), + dbc.Input(id="user_name_input", type="text", placeholder="Enter User Name", + size="sm", style={"width": "300px", }, ), + ], + width="auto", + ), + + dbc.Col( + [ + html.Label("Name", + style={"fontWeight": "bold", "marginBottom": "4px", }, ), + dbc.Input(id="name_input", type="text", placeholder="Enter Name", + size="sm", style={"width": "300px", }, ), + ], + width="auto", + ), + + dbc.Col( + [ + html.Label( + "Password", + style={"fontWeight": "bold", "marginBottom": "4px"}, + ), + dbc.Input( + id="password_input", + type="password", + placeholder="Enter Password", + size="sm", + style={"width": "300px"}, + ), + ], + width="auto", + ), + + dbc.Col( + [ + html.Label( + "Role", + style={"fontWeight": "bold", "marginBottom": "4px"}, + ), + dbc.Select( + id="role_select", + options=[ + {"label": "Matching Engine", "value": "MATCHING_ENGINE"}, + {"label": "Trader", "value": "TRADER"}, + ], + value="TRADER", + placeholder="Select Role", + size="sm", + style={"width": "300px"}, + ), + ], + width="auto", + ), + + dbc.Col( + [ + html.Button("Add", id="user_add", className="btn btn-dark btn-sm me-2", ), + html.Button("Remove", id="user_remove", className="btn btn-dark btn-sm", ), + ], + width="auto", + className="d-flex align-items-end", + ), + ], + className="align-items-end", + ), + ], + ), + + # ------------------------------------------------- + # Error message + # ------------------------------------------------- + + html.Div( + id="user_error", + style={ + "color": "#dc3545", + "marginTop": "5px", + }, + ), + ], + **{"data-ag-theme-mode": "dark"} # Unpacks the dictionary as an HTML keyword attribute + ) + + def _register_callbacks(self): + @self.app.callback( + Output("users_grid_id", "rowData"), + + Output("user_name_input", "value"), + Output("name_input", "value"), + Output("password_input", "value"), + Output("role_select", "value"), + + Input("database-sqlite", "children"), + Input("selected-group", "data"), + prevent_initial_call=True, + ) + def select_first_row(database_sqlite, selected_group): + + if not selected_group: + return [], '', False, '', '', '' + + user_group = selected_group[0] + + users = db_connection.get_group_users(database_sqlite, user_group['user_group']) + + users_out = [] + for user in users: + properties = json.loads(user.get('properties',{})) + user_out = user.copy() + user_out.update({'name':properties.get('name',''), 'type':properties.get('type','')}) + users_out.append(user_out) + + return users_out, '', '', '', 'TRADER' + + @self.app.callback( + Output("user_name_input", "value", allow_duplicate=True), + Output("name_input", "value", allow_duplicate=True), + Output("password_input", "value", allow_duplicate=True), + Output("role_select", "value", allow_duplicate=True), + Input("users_grid_id", "selectedRows"), + prevent_initial_call=True, + ) + def populate_user_inputs(selected_rows): + + if not selected_rows: + return "", "", "", "TRADER" + + user = selected_rows[0] + + return user.get("user_name", ""), user.get("name", ""), "", user.get("type", "TRADER"), + + @self.app.callback( + Output("user_name_input", "value", allow_duplicate=True, ), + Output("user_error", "children", allow_duplicate=True, ), + Output("selected-group", "data", allow_duplicate=True,), + Input("user_add", "n_clicks", ), + State("user_name_input", "value", ), + State("selected-group", "data"), + State("name_input", "value", ), + State("password_input", "value", ), + State("role_select", "value", ), + State("database-sqlite", "children"), + prevent_initial_call=True, + ) + def add_user(add_clicks, user_name_input, selected_group, name_input, password_input, role_select, database_sqlite): + + user_name_input = (user_name_input or "").strip().upper() + + logger.info(f"adding user: {user_name_input}") + + if not user_name_input: + return user_name_input, "Username is required.", selected_group + + user_props = {'name': name_input, 'type': role_select, 'password':password_input} + db_connection.insert_user(database_sqlite, user_name_input, + selected_group[0]['user_group'], user_props) + + return user_name_input, "", selected_group + + @self.app.callback( + Output("user_error", "children", allow_duplicate=True, ), + Output("database-sqlite", "children", allow_duplicate=True, ), + Input("user_remove", "n_clicks", ), + State("user_name_input", "value", ), + State("database-sqlite", "children"), + prevent_initial_call=True, + ) + def user_remove(user_clicks, user_name_input, database_sqlite): + + user_name_input = (user_name_input or "").strip().upper() + + logger.info(f"deleting user: {user_name_input}") + + if not user_name_input: + return "User Name Name is required.", database_sqlite + + db_connection.delete_user(database_sqlite, user_name_input) + + return "", database_sqlite diff --git a/DataService/sql/FrontEnd/data_service_front_end.py b/DataService/sql/FrontEnd/data_service_front_end.py new file mode 100644 index 00000000..283209cb --- /dev/null +++ b/DataService/sql/FrontEnd/data_service_front_end.py @@ -0,0 +1,383 @@ +# Copyright (c) Mike Kipnis - DistributedATS + +import argparse +import logging +import os +import shutil + +import dash +import dash_bootstrap_components as dbc +from dash import html, dcc, Input, Output + +from components import ( + user_group_panel, + markets_panel, + user_panel, + instruments_panel, + instrument_details_panel, + user_group_markets_panel, +) + +import db_connection + +# ---------------------------- +# Logging configuration +# ---------------------------- +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[ + logging.StreamHandler(), + logging.FileHandler("data_service_front_end.log", "a"), + ], +) + +logger = logging.getLogger("DataServiceFrontEnd") + + +# ---------------------------- +# Command-line arguments +# ---------------------------- +parser = argparse.ArgumentParser() + +parser.add_argument( + "--database", + default="distributed_ats.db", + help="Source SQLite database", +) + +args = parser.parse_args() + + +# ---------------------------- +# Database paths +# ---------------------------- +original_database = db_connection.get_database_location( args.database ) +base, ext = os.path.splitext(original_database) +working_database = f"{base}_working{ext}" + +# ---------------------------- +# Database management +# ---------------------------- +def reset_working_database(): + """ + Recreate the working database from the original database. + This is called when the browser loads/reloads the application. + """ + try: + # Remove the old working database first + if os.path.exists(working_database): + os.remove(working_database) + + # Create a fresh working copy + shutil.copy2( original_database, working_database, ) + + logger.info( "Working database reset: %s -> %s", original_database, working_database, ) + + return working_database + + except Exception: + logger.exception("Failed to create working database") + raise + + +def save_working_database(): + """ + Replace the original database with the current + working database. + """ + try: + shutil.copy2(working_database, original_database,) + + logger.info("Database saved: %s -> %s", working_database, original_database, ) + + except Exception: + logger.exception("Failed to save database") + raise + + +# ---------------------------- +# DataServiceFrontEnd class +# ---------------------------- +class DataServiceFrontEnd: + + def __init__(self, app: dash.Dash): + + self.app = app + self.user_groups = user_group_panel.UserGroupPanel(self.app) + self.users = user_panel.UsersPanel(self.app) + self.instrument_groups = markets_panel.MarketsPanel(self.app) + self.instruments = instruments_panel.InstrumentsPanel(self.app) + self.instrument_details = instrument_details_panel.InstrumentDetailsPanel( self.app) + self.user_group_markets_panel = user_group_markets_panel.UserGroupMarketsPanel( self.app ) + + def layout(self): + + return dbc.Container( + [ + + # ================================================== + # Navbar + # ================================================== + dbc.Navbar( + dbc.Container( + [ + + # ---------------------------- + # Left side + # ---------------------------- + dbc.NavbarBrand( + "DistributedATS - Data Service Front-End", + href="#", + style={ "fontSize": "20px" }, + ), + + # ---------------------------- + # Right side + # ---------------------------- + html.Div( + [ + html.Div( + [ + html.Div( + [ + dbc.Label( + id="database-sqlite", + className="navbar-label mb-0 me-2", + ), + dbc.Button( + "Save", + id="save-database", + color="success", + size="sm", + className="py-0 px-2", + style={ + "fontSize": "15px", + "height": "22px", + "lineHeight": "18px", + }, + ), + ], + className="d-flex align-items-center", + ), + + html.Div( + id="status", + className="text-white ms-2", + ), + ], + className="d-flex align-items-center ms-auto", + ), + + ], + className=( + "d-flex flex-column " + "align-items-start ms-auto" + ), + ), + + ], + fluid=True, + ), + color="#0f2538", + dark=True, + ), + + # ================================================== + # Main content + # ================================================== + html.Div( + [ + + dbc.Accordion( + [ + + # ---------------------------- + # Markets and Instruments + # ---------------------------- + dbc.AccordionItem( + dbc.Row( + [ + dbc.Col( + self.instrument_groups.layout() + ), + + dbc.Col( + self.instruments.layout() + ), + + dbc.Col( + self.instrument_details.layout() + ), + ], + className="p-3", + ), + title="Markets and Instruments", + item_id="institution-stock", + ), + + # ---------------------------- + # User Groups and Users + # ---------------------------- + dbc.AccordionItem( + dbc.Row( + [ + dbc.Col( + self.user_groups.layout() + ), + + dbc.Col( + self.users.layout() + ), + + dbc.Col( + self.user_group_markets_panel.layout() + ), + ], + className="p-3", + ), + title="User Groups and Users", + item_id="sector-industry", + ), + + ], + id="main-accordion", + start_collapsed=False, + flush=True, + always_open=True, + active_item=[ + "sector-industry", + "institution-stock", + ], + ), + + ], + id="accordion-wrapper", + ), + + # ================================================== + # Startup trigger + # ================================================== + dcc.Interval(id="startup", n_intervals=0, max_intervals=1,), + + # ================================================== + # Stores + # ================================================== + dcc.Store( id="database-name" ), + dcc.Store( id="user-groups" ), + dcc.Store( id="selected-group" ), + dcc.Store( id="markets" ), + dcc.Store( id="selected-market" ), + dcc.Store( id="selected-instrument" ), + ], + fluid=True, + className="p-0", + ) + + +# ================================================== +# Dash App Initialization +# ================================================== +app = dash.Dash( + __name__, + title="DistributedATS - Data Service Front-End", + external_stylesheets=[dbc.themes.DARKLY], +) + +server = app.server + +# ================================================== +# Initialize Front End +# ================================================== +data_service_front_end = DataServiceFrontEnd(app) + +app.layout = data_service_front_end.layout + +# ================================================== +# Database Callbacks +# ================================================== + +# ---------------------------- +# Browser load / reload +# ---------------------------- +@app.callback( + Output("database-sqlite","children",), + Input("startup","n_intervals",), +) +def set_data_service_data(_): + # Create a fresh working database. + # + # IMPORTANT: + # This does NOT modify the original database. + # + # Browser reload: + # + # original.db + # | + # v + # working.db + # + return reset_working_database() + + +# ---------------------------- +# Save working database +# ---------------------------- +@app.callback( + Output("status", "children",), + Input("save-database","n_clicks",), + prevent_initial_call=True, +) +def save_database(n_clicks): + try: + save_working_database() + return "Database saved" + + except Exception as e: + return f"Save failed: {e}" + + +# ================================================== +# Data Callbacks +# ================================================== + +@app.callback( + Output("user-groups","data",), + Input("database-sqlite","children",), +) +def get_user_groups_data(database_sqlite): + + if not database_sqlite: + return [] + + return db_connection.get_user_groups( + database_sqlite + ) + + +@app.callback( + Output("markets","data",), + Input("database-sqlite","children",), +) +def get_markets(database_sqlite): + + if not database_sqlite: + return [] + + return db_connection.get_markets( + database_sqlite + ) + + +# ================================================== +# Main +# ================================================== +if __name__ == "__main__": + + host = os.getenv( "DASH_HOST", "127.0.0.1", ) + port = int( os.getenv( "DASH_PORT", "8050", )) + + debug = (os.getenv( "DASH_DEBUG", "true", ).lower() == "true") + + app.run( host=host, port=port, debug=debug, use_reloader=False,) \ No newline at end of file diff --git a/DataService/sql/FrontEnd/db_connection.py b/DataService/sql/FrontEnd/db_connection.py new file mode 100644 index 00000000..a8de01db --- /dev/null +++ b/DataService/sql/FrontEnd/db_connection.py @@ -0,0 +1,138 @@ +import json +import logging +import sqlite3 + +logger = logging.getLogger("db_connection") + + +def execute(conn, sql, parameters=()): + logger.info("Executing SQL: %s | Parameters: %s", sql, parameters) + cursor = conn.execute(sql, parameters) + logger.info("SQL execution completed successfully") + return cursor + + +def get_database_location(database_file): + with sqlite3.connect(database_file) as conn: + conn.row_factory = sqlite3.Row + cursor = execute(conn, "PRAGMA database_list") + database_name = cursor.fetchone()[2] + return database_name + + +def get_user_groups(database_file): + with sqlite3.connect(database_file) as conn: + conn.row_factory = sqlite3.Row + cursor = execute(conn, "SELECT * FROM user_group") + user_groups = [dict(row) for row in cursor.fetchall()] + return user_groups + + +def get_markets(database_file): + with sqlite3.connect(database_file) as conn: + conn.row_factory = sqlite3.Row + cursor = execute(conn, "SELECT * FROM market") + markets = [dict(row) for row in cursor.fetchall()] + return markets + + +def get_group_users(database_file, group_name): + with sqlite3.connect(database_file) as conn: + conn.row_factory = sqlite3.Row + cursor = execute(conn, "SELECT * FROM user_code WHERE user_group = ?", (group_name,)) + group_users = [dict(row) for row in cursor.fetchall()] + return group_users + + +def get_markets_for_user_group(database_file, group_name): + with sqlite3.connect(database_file) as conn: + conn.row_factory = sqlite3.Row + cursor = execute(conn, "SELECT * FROM user_group_market_map WHERE user_group = ?", (group_name,)) + markets = [dict(row) for row in cursor.fetchall()] + return markets + + +def get_instruments_for_market(database_file, market): + with sqlite3.connect(database_file) as conn: + conn.row_factory = sqlite3.Row + cursor = execute(conn, "SELECT * FROM instrument_market_map WHERE market_name = ?", (market,)) + instruments = [dict(row) for row in cursor.fetchall()] + return instruments + + +def get_details_for_instrument(database_file, instrument_name): + with sqlite3.connect(database_file) as conn: + conn.row_factory = sqlite3.Row + cursor = execute(conn, "SELECT * FROM instrument WHERE instrument_name = ?", (instrument_name,)) + instrument_details = [dict(row) for row in cursor.fetchall()] + return instrument_details + + +def ins_details_for_instrument(database_file, instrument_name, instrument_details): + instrument_details = instrument_details or {} + with sqlite3.connect(database_file) as conn: + execute(conn, "REPLACE INTO instrument (instrument_name, properties) VALUES (?, ?)", (instrument_name, json.dumps(instrument_details))) + return {"instrument_name": instrument_name, "instrument_details": instrument_details} + + +def insert_market(database_file, market, properties=None): + properties = properties or {} + with sqlite3.connect(database_file) as conn: + execute(conn, "REPLACE INTO market (market_name, properties) VALUES (?, ?)", (market, json.dumps(properties))) + return {"market_name": market, "properties": properties} + + +def delete_market(database_file, market): + with sqlite3.connect(database_file) as conn: + execute(conn, "DELETE FROM market WHERE market_name = ?", (market,)) + return {"market_name": market} + + +def delete_user(database_file, user_name): + with sqlite3.connect(database_file) as conn: + execute(conn, "DELETE FROM user_code WHERE user_name = ?", (user_name,)) + return {"user_name": user_name} + + +def insert_instrument_into_market(database_file, instrument, market): + with sqlite3.connect(database_file) as conn: + execute(conn, "REPLACE INTO instrument_market_map (instrument_name, market_name) VALUES (?, ?)", (instrument, market)) + return {"operation": "insert", "instrument": instrument, "market": market} + + +def delete_instrument_from_market(database_file, instrument, market): + with sqlite3.connect(database_file) as conn: + execute(conn, "DELETE FROM instrument_market_map WHERE instrument_name = ? AND market_name = ?", (instrument, market)) + return {"operation": "delete", "instrument": instrument, "market": market} + + +def insert_user_group(database_file, user_group, properties=None): + properties = properties or {} + with sqlite3.connect(database_file) as conn: + execute(conn, "REPLACE INTO user_group (user_group, properties) VALUES (?, ?)", (user_group, json.dumps(properties))) + return {"user_group": user_group, "properties": properties} + +def remove_user_group(database_file, user_group, properties=None): + properties = properties or {} + with sqlite3.connect(database_file) as conn: + execute(conn, "DELETE FROM user_group where user_group=?", (user_group,)) + return {"user_group": user_group, "properties": properties} + + +def insert_user(database_file, user_name, user_group, properties=None): + properties = properties or {} + with sqlite3.connect(database_file) as conn: + execute(conn, "REPLACE INTO user_code (user_name, user_group, properties) VALUES (?, ?, ?)", (user_name, user_group, json.dumps(properties))) + return {"user_name": user_name, "user_group": user_group, "properties": properties} + + +def set_market_for_user_group(database_file, user_group, market_name): + with sqlite3.connect(database_file) as conn: + execute(conn, "REPLACE INTO user_group_market_map (user_group, market_name) VALUES (?, ?)", (user_group, market_name)) + return {"user_group": user_group, "market_name": market_name} + + +def remove_market_for_user_group(database_file, user_group, market_name): + with sqlite3.connect(database_file) as conn: + execute(conn, "DELETE FROM user_group_market_map WHERE user_group = ? AND market_name = ?", (user_group, market_name)) + return {"user_group": user_group, "market_name": market_name} \ No newline at end of file diff --git a/DataService/sql/FrontEnd/requirements.in b/DataService/sql/FrontEnd/requirements.in new file mode 100644 index 00000000..f20fb214 --- /dev/null +++ b/DataService/sql/FrontEnd/requirements.in @@ -0,0 +1,5 @@ +dash +dash-bootstrap-components +dash-ag-grid +dash-extensions +gunicorn \ No newline at end of file diff --git a/DataService/sql/FrontEnd/requirements.txt b/DataService/sql/FrontEnd/requirements.txt new file mode 100644 index 00000000..49c97fd7 --- /dev/null +++ b/DataService/sql/FrontEnd/requirements.txt @@ -0,0 +1,101 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile requirements.in +# +annotated-types==0.7.0 + # via pydantic +blinker==1.9.0 + # via flask +cachelib==0.14.0 + # via flask-caching +certifi==2026.7.22 + # via requests +charset-normalizer==3.5.1 + # via requests +click==8.1.8 + # via flask +dash==4.0.0 + # via + # -r requirements.in + # dash-ag-grid + # dash-bootstrap-components + # dash-extensions +dash-ag-grid==33.3.3 + # via -r requirements.in +dash-bootstrap-components==2.0.4 + # via -r requirements.in +dash-extensions==1.0.20 + # via -r requirements.in +dataclass-wizard==0.30.1 + # via dash-extensions +editorconfig==0.17.1 + # via jsbeautifier +flask==3.1.3 + # via + # dash + # flask-caching +flask-caching==2.3.1 + # via dash-extensions +gunicorn==23.0.0 + # via -r requirements.in +idna==3.19 + # via requests +importlib-metadata==8.7.1 + # via + # dash + # flask +itsdangerous==2.2.0 + # via flask +jinja2==3.1.6 + # via flask +jsbeautifier==1.15.4 + # via dash-extensions +markupsafe==3.0.3 + # via + # flask + # jinja2 + # werkzeug +more-itertools==10.8.0 + # via dash-extensions +narwhals==2.21.0 + # via plotly +nest-asyncio==1.6.0 + # via dash +packaging==26.3 + # via + # gunicorn + # plotly +plotly==6.9.0 + # via dash +pydantic==2.13.4 + # via dash-extensions +pydantic-core==2.46.4 + # via pydantic +requests==2.32.5 + # via dash +retrying==1.4.2 + # via dash +six==1.17.0 + # via jsbeautifier +typing-extensions==4.16.0 + # via + # dash + # dataclass-wizard + # pydantic + # pydantic-core + # typing-inspection +typing-inspection==0.4.2 + # via pydantic +urllib3==2.6.3 + # via requests +werkzeug==3.1.8 + # via + # dash + # flask +zipp==3.23.1 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/MiscClients/cpp_ws_reactjs/dash_trader/dash_trader.py b/MiscClients/cpp_ws_reactjs/dash_trader/dash_trader.py new file mode 100644 index 00000000..94b89427 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/dash_trader/dash_trader.py @@ -0,0 +1,189 @@ +# Copyright (c) Mike Kipnis - DistributedATS + +import os +import logging +from datetime import datetime, timedelta + +import dash +import dash_ag_grid as dag +import dash_bootstrap_components as dbc +from dash import html, dcc, Output, Input +from components import login_panel +from components import positions_and_market_data +from components import price_level_panel +from components import ticket_panel + +# ---------------------------- +# Logging configuration +# ---------------------------- +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[ + logging.StreamHandler(), + logging.FileHandler("dash_trader.log", "a"), + ], +) + +logger = logging.getLogger("dash_trader") + +# ============================= +# Dash App Initialization +# ============================= +app = dash.Dash( + __name__, + title="Dash Trader", + external_stylesheets=[dag.themes.BASE, dag.themes.BALHAM, dbc.themes.SUPERHERO], +) + +server = app.server # Gunicorn expects this + +class DashTrader: + def __init__(self, app: dash.Dash): + self.app = app + self.login_panel = login_panel.LogonPanel(app) + self.positions_and_market_data_panel = positions_and_market_data.PositionAndMarketDataPanel(app) + self.price_level_panel = price_level_panel.PriceLevelPanel(app) + self.ticket_panel = ticket_panel.TicketPanel(app) + + def layout(self): + return dbc.Container( + [ + dbc.Navbar( + dbc.Container( + [ + # Left side + dbc.NavbarBrand( + "Dash Trader", + href="#", + style={"fontSize": "20px"}, + ), + + # Right side + html.Div( + [ + dbc.Row( + [ + self.login_panel.layout(), + self.ticket_panel.layout() + ] + ), + + html.Div(id="status", className="text-white ms-2"), + ], + className="d-flex flex-column align-items-start ms-auto", + ), + ], + fluid=True, + ), + color="#0f2538", + dark=True, + ), + + html.Div( + [ + dbc.Accordion( + [ + + # --- Sector / Industry --- + dbc.AccordionItem( + dbc.Row( + [ + dbc.Col( + self.positions_and_market_data_panel.layout() + ), + dbc.Col( + dbc.Row( + [ + self.price_level_panel.layout() + ]), + ) + ], + className="p-3", + ), + title="Sector & Industry", + item_id="sector-industry", + ), + + # --- Institution / Stock --- + dbc.AccordionItem( + dbc.Row( + [ + ], + className="p-3", + ), + title="Institution & Stock", + item_id="institution-stock", + ), + + # --- Price Chart / Market Data --- + dbc.AccordionItem( + dbc.Row( + [ + ], + className="p-3 flex-nowrap", # flex-nowrap keeps them on the same row + style={"overflowX": "auto"}, # optional: allows horizontal scroll if content is wide + # no_gutters=True # older versions of dash-bootstrap-components + ), + title="Price Chart & Market Data", + ), + + # --- News --- + dbc.AccordionItem( + dbc.Row( + [ + ], + className="p-3", + ), + title="News", + ), + + ], + id="main-accordion", + start_collapsed=False, # all collapsed on load + flush=True, # cleaner look + always_open=True, # allow multiple open at once + active_item=["sector-industry", "institution-stock"], + )], id="accordion-wrapper",), + + html.Div( + [ + "For support, contact: ", + html.A( + "mike.kipnis@gmail.com", + href="mailto:mike.kipnis@gmail.com", + style={ + "textDecoration": "underline", + "color": "#AAAAAA", + }, + ), + ], + style={ + "fontSize": "12px", + "color": "#AAAAAA", + "marginTop": "8px", + "textAlign": "center", + "width": "100%", + }, + ), + ], + fluid=True, + className="p-0", + ) + +# Create sandbox with this prefix +dash_trader = DashTrader(app) + +# Set layout once +app.layout = dash_trader.layout + +# ---------------------------- +# Main +# ---------------------------- +if __name__ == "__main__": + host = os.getenv("DASH_HOST", "127.0.0.1") + port = int(os.getenv("DASH_PORT", "8050")) + debug = os.getenv("DASH_DEBUG", "true").lower() == "true" + + app.run(host=host, port=port, debug=debug, use_reloader=False) diff --git a/MiscClients/dash_ws/README.md b/MiscClients/dash_ws/README.md new file mode 100644 index 00000000..49084a38 --- /dev/null +++ b/MiscClients/dash_ws/README.md @@ -0,0 +1,9 @@ +## Dash Trader - DistributedATS + +### To build: +``` +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip setuptools wheel +pip install -r requirements.txt +``` diff --git a/MiscClients/dash_ws/components/login_panel.py b/MiscClients/dash_ws/components/login_panel.py new file mode 100644 index 00000000..fe5a4d1b --- /dev/null +++ b/MiscClients/dash_ws/components/login_panel.py @@ -0,0 +1,185 @@ +# Copyright (c) Mike Kipnis - DistributedATS + +import datetime + +import dash +from dash import Input, Output, html, dcc, State +from dash import callback_context +import dash_bootstrap_components as dbc +from dash_extensions import WebSocket + + +class LogonPanel(object): + + def __init__(self, app: dash.Dash): + self.app = app + self._register_callbacks() + + def layout(self): + return dbc.Container( + [ + + # Store + dcc.Store( + id="login-state", + data={ + "text": "Please log in", + "sessionStateCode": 0, + }, + ), + + # Info message (used by callback) + html.Div( + id="info-message", + className="mb-3", + style={"fontWeight": "500"} + ), + + # Form wrapper (used by callback) + html.Div( + id="form-wrapper", + children=[ + dbc.Row( + [ + dcc.Location(id="url", refresh=False), + html.Div(id="ws-url"), + + # Username + dbc.Col( + [ + dbc.Label("Username"), + dbc.Input( + id="username", + type="text", + placeholder="Enter username", + style={ + "backgroundColor": "#0f1118", + "color": "white", + "border": "1px solid #2a2e39", + }, + ), + ], + md=4, + ), + + # Password + dbc.Col( + [ + dbc.Label("Password"), + dbc.Input( + id="password", + type="password", + placeholder="Enter password", + style={ + "backgroundColor": "#0f1118", + "color": "white", + "border": "1px solid #2a2e39", + }, + ), + ], + md=4, + ), + + # Button + dbc.Col( + dbc.Button( + "Login", + id="login-btn", + color="primary", + className="mt-4 w-100", + n_clicks=0, + ), + md=2, + align="end", + ), + ], + className="g-3 align-items-end", + style={"marginTop": "20px"}, + ), + WebSocket(id="ws", url="ws://127.0.0.1:8765") + ], + ), + ], + fluid=True, + ) + + + def _register_callbacks(self): + + @self.app.callback(Output("output", "children"), + Input("ws", "message")) + def display_message(msg): + if msg is None: + return "No data yet" + return f"Received: {msg['data']}" + + @self.app.callback( + Output("ws-url", "children"), + Input("url", "href") + ) + def get_websocket_url(href): + if not href: + return "" + + from urllib.parse import urlparse + + parsed = urlparse(href) + + host = parsed.hostname + port = 9002 + protocol = "wss" if parsed.scheme == "https" else "ws" + + return f"{protocol}://{host}:{port}" + + # ---------------------------- + # Login Submit Logic + # ---------------------------- + @self.app.callback( + Output("login-state", "data"), + Input("login-btn", "n_clicks"), + State("username", "value"), + State("password", "value"), + prevent_initial_call=True, + ) + def handle_login(n, username, password): + if not username or not password: + return { + "text": "Username and password required", + "sessionStateCode": 0 + } + + # This replaces props.logonCallback(...) + logon_value = { + "username": username, + "password": password, + } + + print("Logon value:", logon_value) + + return { + "text": "Ready to trade", + "sessionStateCode": 1 + } + + @self.app.callback( + Output("info-message", "children"), + Output("info-message", "className"), + Output("form-wrapper", "style"), + Input("login-state", "data"), + ) + def update_ui(login_state): + text = login_state["text"] + session_code = login_state["sessionStateCode"] + + # Equivalent to: + # props.loginState.text === "Ready to trade" + class_name = "header_ready_to_trade" if text == "Ready to trade" else "" + + # Equivalent to: + # pointerEvents: "none", opacity: "0.4" + if session_code != 0: + style = {"pointerEvents": "none", "opacity": "0.4"} + else: + style = {} + + return text, class_name, style \ No newline at end of file diff --git a/MiscClients/dash_ws/components/positions_and_market_data.py b/MiscClients/dash_ws/components/positions_and_market_data.py new file mode 100644 index 00000000..c709badf --- /dev/null +++ b/MiscClients/dash_ws/components/positions_and_market_data.py @@ -0,0 +1,57 @@ +import dash +import dash_ag_grid as dag +import dash_bootstrap_components as dbc + +class PositionAndMarketDataPanel(object): + + def __init__(self, app: dash.Dash): + self.app = app + + self.positions_and_market_data = dag.AgGrid( + id="position-and-market-data", + rowData=[], + defaultColDef={ + "resizable": False, + "sortable": False, + "filter": False, + }, + columnDefs=[ + { + "field": "key", + "width": 120, + "cellStyle": {"fontWeight": "bold"}, + }, + { + "field": "value", + "flex": 1, + }, + ], + dashGridOptions={ + "rowSelection": "single", + "animateRows": True, + "suppressMaintainUnsortedOrder": True, + "headerHeight": 0, + "theme": "legacy", + }, + className="ag-theme-balham-dark", + ) + + self._register_callbacks() + + def _register_callbacks(self): + pass + + def layout(self): + return dbc.Container( + [ + dbc.Row( + [ + dbc.Col( + self.positions_and_market_data, + ), + ], + className="p-3", + ), + ], + fluid=True, + ) \ No newline at end of file diff --git a/MiscClients/dash_ws/components/price_level_panel.py b/MiscClients/dash_ws/components/price_level_panel.py new file mode 100644 index 00000000..35af5aaf --- /dev/null +++ b/MiscClients/dash_ws/components/price_level_panel.py @@ -0,0 +1,59 @@ +# Copyright (c) Mike Kipnis - DistributedATS + +import dash +import dash_ag_grid as dag +import dash_bootstrap_components as dbc + +class PriceLevelPanel(object): + + def __init__(self, app: dash.Dash): + self.app = app + + self.price_level = dag.AgGrid( + id="price-level", + rowData=[], + defaultColDef={ + "resizable": False, + "sortable": False, + "filter": False, + }, + columnDefs=[ + { + "field": "key", + "width": 120, + "cellStyle": {"fontWeight": "bold"}, + }, + { + "field": "value", + "flex": 1, + }, + ], + dashGridOptions={ + "rowSelection": "single", + "animateRows": True, + "suppressMaintainUnsortedOrder": True, + "headerHeight": 0, + "theme": "legacy", + }, + className="ag-theme-balham-dark", + ) + + self._register_callbacks() + + def _register_callbacks(self): + pass + + def layout(self): + return dbc.Container( + [ + dbc.Row( + [ + dbc.Col( + self.price_level, + ), + ], + className="p-3", + ), + ], + fluid=True, + ) \ No newline at end of file diff --git a/MiscClients/dash_ws/components/ticket_panel.py b/MiscClients/dash_ws/components/ticket_panel.py new file mode 100644 index 00000000..58407c4d --- /dev/null +++ b/MiscClients/dash_ws/components/ticket_panel.py @@ -0,0 +1,24 @@ +# Copyright (c) Mike Kipnis - DistributedATS + +import datetime + +import dash +from dash import Input, Output, html, dcc, State +from dash import callback_context +import dash_bootstrap_components as dbc + +class TicketPanel(object): + + def __init__(self, app: dash.Dash): + self.app = app + self._register_callbacks() + + def layout(self): + return dbc.Container( + [ + ], + fluid=True, + ) + + def _register_callbacks(self): + pass \ No newline at end of file diff --git a/MiscClients/dash_ws/dash_trader.py b/MiscClients/dash_ws/dash_trader.py new file mode 100644 index 00000000..890611fd --- /dev/null +++ b/MiscClients/dash_ws/dash_trader.py @@ -0,0 +1,208 @@ +# Copyright (c) Mike Kipnis - DistributedATS + +import os +import logging +import threading +from datetime import datetime, timedelta + +import dash +import dash_ag_grid as dag +import dash_bootstrap_components as dbc +from dash import html, dcc, Output, Input +from components import login_panel +from components import positions_and_market_data +from components import price_level_panel +from components import ticket_panel + +from dash_extensions import WebSocket + +import asyncio +import websockets +import json +import random + +from ws_server import ws_main + +# ---------------------------- +# Logging configuration +# ---------------------------- +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[ + logging.StreamHandler(), + logging.FileHandler("dash_trader.log", "a"), + ], +) + +logger = logging.getLogger("dash_trader") + +# ============================= +# Dash App Initialization +# ============================= +app = dash.Dash( + __name__, + title="Dash Trader", + external_stylesheets=[dbc.themes.SUPERHERO], +) + +server = app.server # Gunicorn expects this + +class DashTrader: + def __init__(self, app: dash.Dash): + self.app = app + self.login_panel = login_panel.LogonPanel(app) + self.positions_and_market_data_panel = positions_and_market_data.PositionAndMarketDataPanel(app) + self.price_level_panel = price_level_panel.PriceLevelPanel(app) + self.ticket_panel = ticket_panel.TicketPanel(app) + + def layout(self): + return dbc.Container( + [ + dbc.Navbar( + dbc.Container( + [ + # Left side + dbc.NavbarBrand( + "Dash Trader", + href="#", + style={"fontSize": "20px"}, + ), + + # Right side + html.Div( + [ + dbc.Row( + [ + self.login_panel.layout(), + self.ticket_panel.layout() + ] + ), + + html.Div(id="status", className="text-white ms-2"), + ], + className="d-flex flex-column align-items-start ms-auto", + ), + ], + fluid=True, + ), + color="#0f2538", + dark=True, + ), + + html.Div( + [ + dbc.Accordion( + [ + + # --- Sector / Industry --- + dbc.AccordionItem( + dbc.Row( + [ + dbc.Col( + self.positions_and_market_data_panel.layout() + ), + dbc.Col( + dbc.Row( + [ + self.price_level_panel.layout() + ]), + ) + ], + className="p-3", + ), + title="Sector & Industry", + item_id="sector-industry", + ), + + # --- Institution / Stock --- + dbc.AccordionItem( + dbc.Row( + [ + ], + className="p-3", + ), + title="Institution & Stock", + item_id="institution-stock", + ), + + # --- Price Chart / Market Data --- + dbc.AccordionItem( + dbc.Row( + [ + ], + className="p-3 flex-nowrap", # flex-nowrap keeps them on the same row + style={"overflowX": "auto"}, # optional: allows horizontal scroll if content is wide + # no_gutters=True # older versions of dash-bootstrap-components + ), + title="Price Chart & Market Data", + ), + + # --- News --- + dbc.AccordionItem( + dbc.Row( + [ + ], + className="p-3", + ), + title="News", + ), + + ], + id="main-accordion", + start_collapsed=False, # all collapsed on load + flush=True, # cleaner look + always_open=True, # allow multiple open at once + active_item=["sector-industry", "institution-stock"], + )], id="accordion-wrapper",), + + html.Div( + [ + "For support, contact: ", + html.A( + "mike.kipnis@gmail.com", + href="mailto:mike.kipnis@gmail.com", + style={ + "textDecoration": "underline", + "color": "#AAAAAA", + }, + ), + ], + style={ + "fontSize": "12px", + "color": "#AAAAAA", + "marginTop": "8px", + "textAlign": "center", + "width": "100%", + }, + ), + ], + fluid=True, + className="p-0", + ) + + +def start_ws_server(): + asyncio.run(ws_main()) + +# Create sandbox with this prefix +dash_trader = DashTrader(app) + +# Set layout once +app.layout = dash_trader.layout + +# ---------------------------- +# Main +# ---------------------------- +if __name__ == "__main__": + + # Start WebSocket server in background thread + ws_thread = threading.Thread(target=start_ws_server, daemon=True) + ws_thread.start() + + host = os.getenv("DASH_HOST", "127.0.0.1") + port = int(os.getenv("DASH_PORT", "8050")) + debug = os.getenv("DASH_DEBUG", "true").lower() == "true" + + app.run(host=host, port=port, debug=debug, use_reloader=False) \ No newline at end of file diff --git a/MiscClients/dash_ws/requirements.txt b/MiscClients/dash_ws/requirements.txt new file mode 100644 index 00000000..01bbed93 --- /dev/null +++ b/MiscClients/dash_ws/requirements.txt @@ -0,0 +1,5 @@ +dash>=3.2.0 +dash-bootstrap-components>=2.0.3 +dash-ag-grid>=31.4.0 +dash-extensions +websockets>=12.0 diff --git a/MiscClients/dash_ws/ws_server.py b/MiscClients/dash_ws/ws_server.py new file mode 100644 index 00000000..1cb1ade6 --- /dev/null +++ b/MiscClients/dash_ws/ws_server.py @@ -0,0 +1,68 @@ +import asyncio +import json +import random +import websockets +from websockets.exceptions import ConnectionClosed + +connected = set() + +# ---------------------------- +# Receive loop (client → server) +# ---------------------------- +async def receiver(websocket): + try: + async for message in websocket: + data = json.loads(message) + print("Received:", data) + except ConnectionClosed: + print("Receiver: client disconnected") + + +# ---------------------------- +# Send loop (server → client) +# ---------------------------- +async def sender(websocket): + try: + while True: + data = {"price": random.random()} + await websocket.send(json.dumps(data)) + await asyncio.sleep(1) + except ConnectionClosed: + print("Sender: client disconnected") + + +# ---------------------------- +# Connection handler +# ---------------------------- +async def handler(websocket): + connected.add(websocket) + print("Client connected") + + recv_task = asyncio.create_task(receiver(websocket)) + send_task = asyncio.create_task(sender(websocket)) + + try: + done, pending = await asyncio.wait( + [recv_task, send_task], + return_when=asyncio.FIRST_COMPLETED, + ) + finally: + # Cancel remaining tasks properly + for task in pending: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + connected.remove(websocket) + print("Client disconnected") + + +# ---------------------------- +# Server +# ---------------------------- +async def ws_main(): + async with websockets.serve(handler, "127.0.0.1", 8765): + print("WebSocket server running on ws://127.0.0.1:8765") + await asyncio.Future() \ No newline at end of file