Here is the flow
[
{
"id": "3d4acce1469afe19",
"type": "tab",
"label": "MISC",
"disabled": false,
"info": "",
"env": []
},
{
"id": "19d394250be1db32",
"type": "json",
"z": "3d4acce1469afe19",
"name": "",
"property": "payload",
"action": "",
"pretty": true,
"x": 170,
"y": 220,
"wires": [
[
"460618fdd75e3cae"
]
]
},
{
"id": "0a4476a75d5733b2",
"type": "ui_template",
"z": "3d4acce1469afe19",
"group": "267f752bf969a63c",
"name": "ScreenerM ",
"order": 2,
"width": "9",
"height": "10",
"format": "<script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/luxon@3.0.4/build/global/luxon.min.js\"></script>\n<div id=\"screener\"></div>\n<script>\n var screenerTable = new Tabulator(\"#screener\", {\n \theight:900, \n \theaderSortElement:\"\",\n \tlayout:\"fitColumns\",\n \tcolumns:[ //Define Table Columns\n \t \t{title:\"Stock\", field:\"name\", hozAlign:\"center\", width:58},\n \t \t{title:\"Gap\", field:\"gap\", hozAlign:\"right\", width:65, \n formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return \"\"}; if (value !== null) {if(value >= 0){cell.getElement().style.color ='#609f70'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}}, \t \t\t \t \t \t\n \t \t{title:\"Change\", field:\"change\", hozAlign:\"right\", width:58}, \t \t\n// \t \t{title:\"Change\", field:\"change\", hozAlign:\"right\", width:62, formatterParams:{precision:2},\n// formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return \"\"}; if (value !== null) {if(value >= 0){cell.getElement().style.color ='#609f70'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}}, \n {title:\"Δopen\", field:\"change_from_open\", hozAlign:\"right\", width:58, formatter:\"money\", formatterParams:{precision:1}},\n {title:\"Price\", field:\"close\", hozAlign:\"right\", width:50, formatter:\"money\", formatterParams:{precision:2}},\n \t \t{title:\"rVol%\", field:\"relative_volume_10d_calc\", hozAlign:\"right\", width:60, formatter:\"money\", formatterParams:{precision:1}}\n// \t \t{title:\"TA\", field:\"analysis\", hozAlign:\"center\", width:65,\n// \t \t formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == \"NEUTRAL\"){cell.getElement().style.color ='#787b86'} else if(value == \"BUY\") {cell.getElement().style.color ='green'} else if(value == \"S.BUY\") {cell.getElement().style.color ='#fdef55'} return value;}}, \t \t\n// \t \t{title:\"GN\", field:\"gain\", hozAlign:\"right\", width:55,\n// \t \t formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return \"\"}; if (value !== null) {if(value >= 0){cell.getElement().style.color ='#609f70'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}},\n \t],\n });\n\n\n (function(scope){\n scope.$watch('msg', function(msg){\n screenerTable.setData(msg.payload, true);\n });\n screenerTable.on(\"cellClick\", function(e, cell){\n scope.send({topic:\"sell\", payload:cell.getData()});\n });\n })(scope); \n\n\n</script>",
"storeOutMessages": false,
"fwdInMessages": false,
"resendOnRefresh": false,
"templateScope": "local",
"className": "",
"x": 460,
"y": 180,
"wires": [
[
"25e724798a759460"
]
]
},
{
"id": "02e1e40f779d45cb",
"type": "ui_ui_control",
"z": "3d4acce1469afe19",
"name": "refresh",
"events": "all",
"x": 470,
"y": 280,
"wires": [
[
"25e724798a759460"
]
]
},
{
"id": "3b2a0ebd67833924",
"type": "debug",
"z": "3d4acce1469afe19",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 650,
"y": 220,
"wires": []
},
{
"id": "25e724798a759460",
"type": "function",
"z": "3d4acce1469afe19",
"name": "cache",
"func": "var status = {fill:\"red\",shape:\"ring\",text:\"an error occured\"};\nvar success = (msg.topic && msg.topic===\"success\") || false;\nvar tableData = flow.get(\"tableData\");\nif (tableData === undefined) {\n tableData = [];\n flow.set(\"tableData\",tableData);\n}\n\n// find the index for a row in tableData for a given index (id)\nfunction checkIndex(id) {\n let matchRow=-1\n tableData.forEach(function (row,index){\n if (row.id === id){\n matchRow=index;\n return matchRow;\n }\n })\n return matchRow;\n}\n\n// flat merge one row \nfunction mergeRow(dest,source) {\n Object.keys(source).forEach(function(key) {\n dest[key]=source[key];\n })\n}\n\n//merge or add one or many rows into tableData \nfunction mergeData(newData,toTop) {\n newData.forEach(function (item,index) {\n node.warn([\"findIndex\",item]);\n let row=checkIndex(item.id);\n if (row<0) { // row do not existst in tableData\n if (toTop) {\n tableData.push(item);\n status.text+=\"newRow @ top\";\n } else {\n tableData.unshift(item);\n status.text+=\"newRow @ bottom\";\n }\n return;\n } else { // row exists so update\n mergeRow(tableData[row],item);\n status.text+=\"row updated\";\n return;\n }\n if (status.text!==\"\") node.status(status);\n });\n}\n\nswitch (typeof msg.payload){\n case \"string\":\n node.warn([\"[table recorder] \",(typeof msg.payload),msg.payload]);\n switch (msg.payload){\n case \"change\":\n status={fill:\"green\",shape:\"dot\",text:\"table restored \"+tableData.length+\" rows\"};\n msg.payload=tableData;\n break;\n }\n break;\n case \"object\":\n node.warn([\"[table recorder] \",(typeof msg.payload),msg.payload]);\n if (Array.isArray(msg.payload)) { // replace all tableData\n status={fill:\"green\",shape:\"dot\",text:\"table replaced \"+msg.payload.length+\" rows\"};\n tableData=RED.util.cloneMessage(msg.payload); \n } else {\n switch (msg.payload.command) { // clearData does not return a promise!\n case \"clearData\":\n status={fill:\"green\",shape:\"dot\",text:\"clearData: done\"};\n tableData=[];\n flow.set(\"lastId\",0);\n break; \n }\n }\n break;\n default: // likely a msg fom a ui-table command or callback\n if (msg.hasOwnProperty(\"topic\")&&\n msg.hasOwnProperty(\"ui_control\") && \n msg.ui_control.hasOwnProperty(\"callback\") &&\n msg.hasOwnProperty(\"return\")) { // message originates from a ui-table callback\n if (success) {\n switch(msg.return.command) {\n case \"addRow\":\n status.text=\"addRow: \";\n mergeData(msg.return.arguments[0],msg.return.arguments[1]);\n status.shape=\"dot\";\n break;\n case \"updateOrAddData\":\n status.text=\"updateOrAddData: \";\n mergeData(msg.return.arguments[0]);\n break;\n case \"deleteRow\":\n let row=checkIndex(msg.return.arguments[0]);\n tableData.splice(row,1);\n status.shape=\"dot\";\n status.text=\"deleteRow: \"+row+\" deleted\";\n break;\n default:\n status={fill:\"yellow\",shape:\"dot\",text:msg.return.command + \" unknown!\"};\n break; \n }\n } else {\n status.text=msg.topic+\" \"+msg.error;\n }\n }\n break;\n}\nif (success) status.fill=\"green\";\nflow.set(\"tableData\",tableData);\nnode.status(status);\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 220,
"wires": [
[
"0a4476a75d5733b2",
"3b2a0ebd67833924"
]
],
"icon": "font-awesome/fa-database",
"info": "# simple ui-table handler\n## abstract\nUsing ui-table with commands offer the hole flexibilty of tabulator. The table can be manipulated down to cell level.\nAs the ui-table node only passes the commands to tabulator and receives promises back the node does not hold the table data. If the data should be available after refresh, tab change, new connections the flow is responsible to cache the data and all the manipulations.\nThis node takes care of most simple data manipulation commands and holds a copy of the data in `flow.context.tabledata`\n\n## details\n\n### row index (id)\n\nTo identify a [row a index](http://tabulator.info/docs/4.5/data#overview) column has to be defined. This colum defaults to `id` but can be changed by specifing a **field** by using `msg.ui_control`. In this example the row index is a simple counter adding up by one if a new line is added.\n\n### addRow command\n\n[details @ tabulator addRow docs](http://tabulator.info/docs/4.5/update#alter-add)\n\nYou can add a row by sending the `addRow` command. You can decide if the row adds on the top or at the bottom of table.\n\n### addOrUpdate command\n\n[details @ tabulator addOrUpdate docs](http://tabulator.info/docs/4.5/update#alter-update)\n\nTo update data the best way is to use the `addOrUpdate` command. If the row indetified by the index is not exeisting a new row will be added automatically\n\n### deleteRow command\n\n[details @ tabulator deleteRow docs](http://tabulator.info/docs/4.5/update#row)\n\nDelete one or more rows (passing an array always results in \"row not found error\"! I think there is an issue in tabulator)\n\n### clearData\n\n[details @ tabulator clearData docs](http://tabulator.info/docs/4.5/update#alter-empty)\n\nunfortunately this command (currently) do not send a promise back! So we have to pass it directly to the table handler"
},
{
"id": "460618fdd75e3cae",
"type": "change",
"z": "3d4acce1469afe19",
"name": "Round",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "$$.payload.{\t \t \"ticker\": $.ticker,\t \"name\": $.name,\t \"close\": $round($.close,1),\t \"gap\": $round($.gap,1),\t \"relative_volume_10d_calc\": $round($.relative_volume_10d_calc,1),\t \"change\": $round($.change,1),\t \"premarket_change\": $.premarket_change ? $round($.premarket_change,1) : null,\t \"float_shares_outstanding\": $.float_shares_outstanding ? $round($.float_shares_outstanding,0) : null,\t \"change_from_open\": $round($.change_from_open,1)\t \t}\t\t",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 310,
"y": 220,
"wires": [
[
"25e724798a759460"
]
]
},
{
"id": "267f752bf969a63c",
"type": "ui_group",
"name": "HomeM",
"tab": "11b9c054a98cd693",
"order": 2,
"disp": false,
"width": "9",
"collapse": false,
"className": ""
},
{
"id": "11b9c054a98cd693",
"type": "ui_tab",
"name": "MOBILE",
"icon": "dashboard",
"order": 1,
"disabled": false,
"hidden": false
}
]
Here is a data sample that's coming in
{"_msgid":"21a4fdf66e94edcb","topic":"","filename":"/home/ubuntu/.node-red/node_modules/node-red-dashboard/dist/scripts/scannerM.py","payload":[{"ticker":"NASDAQ:HOLO","name":"HOLO","close":7,"gap":12,"relative_volume_10d_calc":7.8,"change":34.6,"premarket_change":11.6,"float_shares_outstanding":19774512,"change_from_open":20.2},{"ticker":"NASDAQ:BNZI","name":"BNZI","close":4.8,"gap":117.4,"relative_volume_10d_calc":47.2,"change":72.5,"premarket_change":117.4,"float_shares_outstanding":745040,"change_from_open":-20.6},{"ticker":"NASDAQ:IKT","name":"IKT","close":1.4,"gap":27.7,"relative_volume_10d_calc":1291.9,"change":13.5,"premarket_change":26.1,"float_shares_outstanding":6566814,"change_from_open":-11.2},{"ticker":"NASDAQ:NUWE","name":"NUWE","close":1.5,"gap":-1.6,"relative_volume_10d_calc":27.6,"change":23.6,"premarket_change":4.1,"float_shares_outstanding":1121178,"change_from_open":25.6},{"ticker":"NASDAQ:CASI","name":"CASI","close":6.8,"gap":5.3,"relative_volume_10d_calc":7.7,"change":19.8,"premarket_change":null,"float_shares_outstanding":4278223,"change_from_open":13.8},{"ticker":"NASDAQ:CELZ","name":"CELZ","close":3.9,"gap":7.7,"relative_volume_10d_calc":10.1,"change":23.1,"premarket_change":2.2,"float_shares_outstanding":1301358,"change_from_open":14.3},{"ticker":"OTC:MOBQ","name":"MOBQ","close":2.8,"gap":2.1,"relative_volume_10d_calc":6.2,"change":15.9,"premarket_change":null,"float_shares_outstanding":7775477,"change_from_open":13.6},{"ticker":"OTC:DDHRF","name":"DDHRF","close":3,"gap":21.8,"relative_volume_10d_calc":23.5,"change":20,"premarket_change":null,"float_shares_outstanding":11227671,"change_from_open":-1.5}],"rc":{"code":0}}
Here is what it looks like coming out of the cache function.
["[table recorder] ","object",[{"ticker":"NASDAQ:HOLO","name":"HOLO","close":7,"gap":12,"relative_volume_10d_calc":7.8,"change":34.6,"premarket_change":11.6,"float_shares_outstanding":19774512,"change_from_open":20.2},{"ticker":"NASDAQ:BNZI","name":"BNZI","close":4.8,"gap":117.4,"relative_volume_10d_calc":47.2,"change":72.5,"premarket_change":117.4,"float_shares_outstanding":745040,"change_from_open":-20.6},{"ticker":"NASDAQ:IKT","name":"IKT","close":1.4,"gap":27.7,"relative_volume_10d_calc":1291.9,"change":13.5,"premarket_change":26.1,"float_shares_outstanding":6566814,"change_from_open":-11.2},{"ticker":"NASDAQ:NUWE","name":"NUWE","close":1.5,"gap":-1.6,"relative_volume_10d_calc":27.6,"change":23.6,"premarket_change":4.1,"float_shares_outstanding":1121178,"change_from_open":25.6},{"ticker":"NASDAQ:CASI","name":"CASI","close":6.8,"gap":5.3,"relative_volume_10d_calc":7.7,"change":19.8,"premarket_change":null,"float_shares_outstanding":4278223,"change_from_open":13.8},{"ticker":"NASDAQ:CELZ","name":"CELZ","close":3.9,"gap":7.7,"relative_volume_10d_calc":10.1,"change":23.1,"premarket_change":2.2,"float_shares_outstanding":1301358,"change_from_open":14.3},{"ticker":"OTC:MOBQ","name":"MOBQ","close":2.8,"gap":2.1,"relative_volume_10d_calc":6.2,"change":15.9,"premarket_change":null,"float_shares_outstanding":7775477,"change_from_open":13.6},{"ticker":"OTC:DDHRF","name":"DDHRF","close":3,"gap":21.8,"relative_volume_10d_calc":23.5,"change":20,"premarket_change":null,"float_shares_outstanding":11227671,"change_from_open":-1.5}]]
So as the data is coming into the cache function the "name" variable should be compared and if there is no match (i.e.. it's new) it sends a notification with the details.