瀏覽代碼

feat: Add WebSocket real-time support with Socket.io

- Integrate Socket.io server with HTTP server in src/server.js
- Add WebSocket event emission in live price controller for real-time price updates
- Add WebSocket event emission in candle controller for real-time candle updates
- Implement client subscription/unsubscription system for symbols
- Add socket.io and socket.io-client dependencies
- Update server port configuration to 3001
- Maintain backward compatibility with existing REST APIs
- Support multiple concurrent client connections for InsightBull projects
uzairrizwan1 3 月之前
父節點
當前提交
9244a27a2c
共有 6 個文件被更改,包括 513 次插入2 次删除
  1. 1 1
      README.md
  2. 341 0
      package-lock.json
  3. 2 0
      package.json
  4. 64 0
      src/controllers/candleController.js
  5. 50 0
      src/controllers/livePriceController.js
  6. 55 1
      src/server.js

+ 1 - 1
README.md

@@ -658,7 +658,7 @@ DB_USER=your_username
658 658
 DB_PASSWORD=your_password
659 659
 
660 660
 # Server Configuration
661
-PORT=3000
661
+PORT=3001
662 662
 NODE_ENV=development
663 663
 
664 664
 # JWT Configuration (if needed for authentication)

+ 341 - 0
package-lock.json

@@ -17,6 +17,8 @@
17 17
         "morgan": "^1.10.1",
18 18
         "pg": "^8.16.3",
19 19
         "sequelize": "^6.37.7",
20
+        "socket.io": "^4.8.1",
21
+        "socket.io-client": "^4.8.1",
20 22
         "winston": "^3.18.3"
21 23
       },
22 24
       "devDependencies": {
@@ -1187,6 +1189,12 @@
1187 1189
         "text-hex": "1.0.x"
1188 1190
       }
1189 1191
     },
1192
+    "node_modules/@socket.io/component-emitter": {
1193
+      "version": "3.1.2",
1194
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
1195
+      "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
1196
+      "license": "MIT"
1197
+    },
1190 1198
     "node_modules/@standard-schema/spec": {
1191 1199
       "version": "1.0.0",
1192 1200
       "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
@@ -1249,6 +1257,15 @@
1249 1257
         "@babel/types": "^7.28.2"
1250 1258
       }
1251 1259
     },
1260
+    "node_modules/@types/cors": {
1261
+      "version": "2.8.19",
1262
+      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
1263
+      "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
1264
+      "license": "MIT",
1265
+      "dependencies": {
1266
+        "@types/node": "*"
1267
+      }
1268
+    },
1252 1269
     "node_modules/@types/debug": {
1253 1270
       "version": "4.1.12",
1254 1271
       "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -1887,6 +1904,15 @@
1887 1904
       "dev": true,
1888 1905
       "license": "MIT"
1889 1906
     },
1907
+    "node_modules/base64id": {
1908
+      "version": "2.0.0",
1909
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
1910
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
1911
+      "license": "MIT",
1912
+      "engines": {
1913
+        "node": "^4.5.0 || >= 5.9"
1914
+      }
1915
+    },
1890 1916
     "node_modules/baseline-browser-mapping": {
1891 1917
       "version": "2.8.17",
1892 1918
       "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.17.tgz",
@@ -2661,6 +2687,125 @@
2661 2687
         "node": ">= 0.8"
2662 2688
       }
2663 2689
     },
2690
+    "node_modules/engine.io": {
2691
+      "version": "6.6.4",
2692
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
2693
+      "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
2694
+      "license": "MIT",
2695
+      "dependencies": {
2696
+        "@types/cors": "^2.8.12",
2697
+        "@types/node": ">=10.0.0",
2698
+        "accepts": "~1.3.4",
2699
+        "base64id": "2.0.0",
2700
+        "cookie": "~0.7.2",
2701
+        "cors": "~2.8.5",
2702
+        "debug": "~4.3.1",
2703
+        "engine.io-parser": "~5.2.1",
2704
+        "ws": "~8.17.1"
2705
+      },
2706
+      "engines": {
2707
+        "node": ">=10.2.0"
2708
+      }
2709
+    },
2710
+    "node_modules/engine.io-client": {
2711
+      "version": "6.6.3",
2712
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
2713
+      "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
2714
+      "license": "MIT",
2715
+      "dependencies": {
2716
+        "@socket.io/component-emitter": "~3.1.0",
2717
+        "debug": "~4.3.1",
2718
+        "engine.io-parser": "~5.2.1",
2719
+        "ws": "~8.17.1",
2720
+        "xmlhttprequest-ssl": "~2.1.1"
2721
+      }
2722
+    },
2723
+    "node_modules/engine.io-client/node_modules/debug": {
2724
+      "version": "4.3.7",
2725
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
2726
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
2727
+      "license": "MIT",
2728
+      "dependencies": {
2729
+        "ms": "^2.1.3"
2730
+      },
2731
+      "engines": {
2732
+        "node": ">=6.0"
2733
+      },
2734
+      "peerDependenciesMeta": {
2735
+        "supports-color": {
2736
+          "optional": true
2737
+        }
2738
+      }
2739
+    },
2740
+    "node_modules/engine.io-parser": {
2741
+      "version": "5.2.3",
2742
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
2743
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
2744
+      "license": "MIT",
2745
+      "engines": {
2746
+        "node": ">=10.0.0"
2747
+      }
2748
+    },
2749
+    "node_modules/engine.io/node_modules/accepts": {
2750
+      "version": "1.3.8",
2751
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
2752
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
2753
+      "license": "MIT",
2754
+      "dependencies": {
2755
+        "mime-types": "~2.1.34",
2756
+        "negotiator": "0.6.3"
2757
+      },
2758
+      "engines": {
2759
+        "node": ">= 0.6"
2760
+      }
2761
+    },
2762
+    "node_modules/engine.io/node_modules/debug": {
2763
+      "version": "4.3.7",
2764
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
2765
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
2766
+      "license": "MIT",
2767
+      "dependencies": {
2768
+        "ms": "^2.1.3"
2769
+      },
2770
+      "engines": {
2771
+        "node": ">=6.0"
2772
+      },
2773
+      "peerDependenciesMeta": {
2774
+        "supports-color": {
2775
+          "optional": true
2776
+        }
2777
+      }
2778
+    },
2779
+    "node_modules/engine.io/node_modules/mime-db": {
2780
+      "version": "1.52.0",
2781
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
2782
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
2783
+      "license": "MIT",
2784
+      "engines": {
2785
+        "node": ">= 0.6"
2786
+      }
2787
+    },
2788
+    "node_modules/engine.io/node_modules/mime-types": {
2789
+      "version": "2.1.35",
2790
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
2791
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
2792
+      "license": "MIT",
2793
+      "dependencies": {
2794
+        "mime-db": "1.52.0"
2795
+      },
2796
+      "engines": {
2797
+        "node": ">= 0.6"
2798
+      }
2799
+    },
2800
+    "node_modules/engine.io/node_modules/negotiator": {
2801
+      "version": "0.6.3",
2802
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
2803
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
2804
+      "license": "MIT",
2805
+      "engines": {
2806
+        "node": ">= 0.6"
2807
+      }
2808
+    },
2664 2809
     "node_modules/error-ex": {
2665 2810
       "version": "1.3.4",
2666 2811
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
@@ -5906,6 +6051,173 @@
5906 6051
         "node": ">=8"
5907 6052
       }
5908 6053
     },
6054
+    "node_modules/socket.io": {
6055
+      "version": "4.8.1",
6056
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
6057
+      "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
6058
+      "license": "MIT",
6059
+      "dependencies": {
6060
+        "accepts": "~1.3.4",
6061
+        "base64id": "~2.0.0",
6062
+        "cors": "~2.8.5",
6063
+        "debug": "~4.3.2",
6064
+        "engine.io": "~6.6.0",
6065
+        "socket.io-adapter": "~2.5.2",
6066
+        "socket.io-parser": "~4.2.4"
6067
+      },
6068
+      "engines": {
6069
+        "node": ">=10.2.0"
6070
+      }
6071
+    },
6072
+    "node_modules/socket.io-adapter": {
6073
+      "version": "2.5.5",
6074
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
6075
+      "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
6076
+      "license": "MIT",
6077
+      "dependencies": {
6078
+        "debug": "~4.3.4",
6079
+        "ws": "~8.17.1"
6080
+      }
6081
+    },
6082
+    "node_modules/socket.io-adapter/node_modules/debug": {
6083
+      "version": "4.3.7",
6084
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
6085
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
6086
+      "license": "MIT",
6087
+      "dependencies": {
6088
+        "ms": "^2.1.3"
6089
+      },
6090
+      "engines": {
6091
+        "node": ">=6.0"
6092
+      },
6093
+      "peerDependenciesMeta": {
6094
+        "supports-color": {
6095
+          "optional": true
6096
+        }
6097
+      }
6098
+    },
6099
+    "node_modules/socket.io-client": {
6100
+      "version": "4.8.1",
6101
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
6102
+      "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
6103
+      "license": "MIT",
6104
+      "dependencies": {
6105
+        "@socket.io/component-emitter": "~3.1.0",
6106
+        "debug": "~4.3.2",
6107
+        "engine.io-client": "~6.6.1",
6108
+        "socket.io-parser": "~4.2.4"
6109
+      },
6110
+      "engines": {
6111
+        "node": ">=10.0.0"
6112
+      }
6113
+    },
6114
+    "node_modules/socket.io-client/node_modules/debug": {
6115
+      "version": "4.3.7",
6116
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
6117
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
6118
+      "license": "MIT",
6119
+      "dependencies": {
6120
+        "ms": "^2.1.3"
6121
+      },
6122
+      "engines": {
6123
+        "node": ">=6.0"
6124
+      },
6125
+      "peerDependenciesMeta": {
6126
+        "supports-color": {
6127
+          "optional": true
6128
+        }
6129
+      }
6130
+    },
6131
+    "node_modules/socket.io-parser": {
6132
+      "version": "4.2.4",
6133
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
6134
+      "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
6135
+      "license": "MIT",
6136
+      "dependencies": {
6137
+        "@socket.io/component-emitter": "~3.1.0",
6138
+        "debug": "~4.3.1"
6139
+      },
6140
+      "engines": {
6141
+        "node": ">=10.0.0"
6142
+      }
6143
+    },
6144
+    "node_modules/socket.io-parser/node_modules/debug": {
6145
+      "version": "4.3.7",
6146
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
6147
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
6148
+      "license": "MIT",
6149
+      "dependencies": {
6150
+        "ms": "^2.1.3"
6151
+      },
6152
+      "engines": {
6153
+        "node": ">=6.0"
6154
+      },
6155
+      "peerDependenciesMeta": {
6156
+        "supports-color": {
6157
+          "optional": true
6158
+        }
6159
+      }
6160
+    },
6161
+    "node_modules/socket.io/node_modules/accepts": {
6162
+      "version": "1.3.8",
6163
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
6164
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
6165
+      "license": "MIT",
6166
+      "dependencies": {
6167
+        "mime-types": "~2.1.34",
6168
+        "negotiator": "0.6.3"
6169
+      },
6170
+      "engines": {
6171
+        "node": ">= 0.6"
6172
+      }
6173
+    },
6174
+    "node_modules/socket.io/node_modules/debug": {
6175
+      "version": "4.3.7",
6176
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
6177
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
6178
+      "license": "MIT",
6179
+      "dependencies": {
6180
+        "ms": "^2.1.3"
6181
+      },
6182
+      "engines": {
6183
+        "node": ">=6.0"
6184
+      },
6185
+      "peerDependenciesMeta": {
6186
+        "supports-color": {
6187
+          "optional": true
6188
+        }
6189
+      }
6190
+    },
6191
+    "node_modules/socket.io/node_modules/mime-db": {
6192
+      "version": "1.52.0",
6193
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
6194
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
6195
+      "license": "MIT",
6196
+      "engines": {
6197
+        "node": ">= 0.6"
6198
+      }
6199
+    },
6200
+    "node_modules/socket.io/node_modules/mime-types": {
6201
+      "version": "2.1.35",
6202
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
6203
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
6204
+      "license": "MIT",
6205
+      "dependencies": {
6206
+        "mime-db": "1.52.0"
6207
+      },
6208
+      "engines": {
6209
+        "node": ">= 0.6"
6210
+      }
6211
+    },
6212
+    "node_modules/socket.io/node_modules/negotiator": {
6213
+      "version": "0.6.3",
6214
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
6215
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
6216
+      "license": "MIT",
6217
+      "engines": {
6218
+        "node": ">= 0.6"
6219
+      }
6220
+    },
5909 6221
     "node_modules/source-map": {
5910 6222
       "version": "0.6.1",
5911 6223
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -6745,6 +7057,35 @@
6745 7057
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
6746 7058
       }
6747 7059
     },
7060
+    "node_modules/ws": {
7061
+      "version": "8.17.1",
7062
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
7063
+      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
7064
+      "license": "MIT",
7065
+      "engines": {
7066
+        "node": ">=10.0.0"
7067
+      },
7068
+      "peerDependencies": {
7069
+        "bufferutil": "^4.0.1",
7070
+        "utf-8-validate": ">=5.0.2"
7071
+      },
7072
+      "peerDependenciesMeta": {
7073
+        "bufferutil": {
7074
+          "optional": true
7075
+        },
7076
+        "utf-8-validate": {
7077
+          "optional": true
7078
+        }
7079
+      }
7080
+    },
7081
+    "node_modules/xmlhttprequest-ssl": {
7082
+      "version": "2.1.2",
7083
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
7084
+      "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
7085
+      "engines": {
7086
+        "node": ">=0.4.0"
7087
+      }
7088
+    },
6748 7089
     "node_modules/xtend": {
6749 7090
       "version": "4.0.2",
6750 7091
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

+ 2 - 0
package.json

@@ -30,6 +30,8 @@
30 30
     "morgan": "^1.10.1",
31 31
     "pg": "^8.16.3",
32 32
     "sequelize": "^6.37.7",
33
+    "socket.io": "^4.8.1",
34
+    "socket.io-client": "^4.8.1",
33 35
     "winston": "^3.18.3"
34 36
   },
35 37
   "devDependencies": {

+ 64 - 0
src/controllers/candleController.js

@@ -120,6 +120,29 @@ class CandleController {
120 120
 
121 121
       const candle = await Candle1h.create(candleData);
122 122
 
123
+      // Emit WebSocket event for real-time updates
124
+      const io = req.app.get('io');
125
+      if (io) {
126
+        const eventData = {
127
+          symbol: symbol.symbol,
128
+          symbolId: symbol.id,
129
+          openTime: candle.openTime,
130
+          open: candle.open,
131
+          high: candle.high,
132
+          low: candle.low,
133
+          close: candle.close,
134
+          volume: candle.volume,
135
+          exchange: symbol.exchange,
136
+          instrumentType: symbol.instrumentType
137
+        };
138
+
139
+        // Emit to all clients subscribed to this symbol
140
+        io.to(`symbol:${symbol.symbol}`).emit('candleUpdate', eventData);
141
+
142
+        // Also emit to general candle updates
143
+        io.emit('candleUpdate', eventData);
144
+      }
145
+
123 146
       res.status(201).json({
124 147
         success: true,
125 148
         data: candle,
@@ -159,6 +182,47 @@ class CandleController {
159 182
 
160 183
       const createdCandles = await Candle1h.bulkCreate(candles);
161 184
 
185
+      // Emit WebSocket events for real-time updates
186
+      const io = req.app.get('io');
187
+      if (io) {
188
+        // Group candles by symbol for efficient emission
189
+        const symbolGroups = {};
190
+        for (const candle of createdCandles) {
191
+          if (!symbolGroups[candle.symbolId]) {
192
+            symbolGroups[candle.symbolId] = [];
193
+          }
194
+          symbolGroups[candle.symbolId].push(candle);
195
+        }
196
+
197
+        // Emit events for each symbol
198
+        for (const [symbolId, symbolCandles] of Object.entries(symbolGroups)) {
199
+          const symbol = await Symbol.findByPk(symbolId);
200
+          if (symbol) {
201
+            // Emit the latest candle for this symbol (most recent)
202
+            const latestCandle = symbolCandles.sort((a, b) => new Date(b.openTime) - new Date(a.openTime))[0];
203
+
204
+            const eventData = {
205
+              symbol: symbol.symbol,
206
+              symbolId: symbol.id,
207
+              openTime: latestCandle.openTime,
208
+              open: latestCandle.open,
209
+              high: latestCandle.high,
210
+              low: latestCandle.low,
211
+              close: latestCandle.close,
212
+              volume: latestCandle.volume,
213
+              exchange: symbol.exchange,
214
+              instrumentType: symbol.instrumentType
215
+            };
216
+
217
+            // Emit to all clients subscribed to this symbol
218
+            io.to(`symbol:${symbol.symbol}`).emit('candleUpdate', eventData);
219
+
220
+            // Also emit to general candle updates
221
+            io.emit('candleUpdate', eventData);
222
+          }
223
+        }
224
+      }
225
+
162 226
       res.status(201).json({
163 227
         success: true,
164 228
         data: createdCandles,

+ 50 - 0
src/controllers/livePriceController.js

@@ -90,6 +90,29 @@ class LivePriceController {
90 90
         lastUpdated: new Date()
91 91
       });
92 92
 
93
+      // Emit WebSocket event for real-time updates
94
+      const io = req.app.get('io');
95
+      if (io) {
96
+        const eventData = {
97
+          symbol: symbol.symbol,
98
+          symbolId: symbol.id,
99
+          price,
100
+          bid,
101
+          ask,
102
+          bidSize,
103
+          askSize,
104
+          lastUpdated: livePrice.lastUpdated,
105
+          exchange: symbol.exchange,
106
+          instrumentType: symbol.instrumentType
107
+        };
108
+
109
+        // Emit to all clients subscribed to this symbol
110
+        io.to(`symbol:${symbol.symbol}`).emit('livePriceUpdate', eventData);
111
+
112
+        // Also emit to general live price updates
113
+        io.emit('livePriceUpdate', eventData);
114
+      }
115
+
93 116
       res.status(created ? 201 : 200).json({
94 117
         success: true,
95 118
         data: livePrice,
@@ -142,9 +165,36 @@ class LivePriceController {
142 165
       }));
143 166
 
144 167
       const updatedPrices = [];
168
+      const io = req.app.get('io');
169
+
145 170
       for (const data of upsertData) {
146 171
         const [livePrice] = await LivePrice.upsert(data);
147 172
         updatedPrices.push(livePrice);
173
+
174
+        // Emit WebSocket event for each updated price
175
+        if (io) {
176
+          const symbol = await Symbol.findByPk(data.symbolId);
177
+          if (symbol) {
178
+            const eventData = {
179
+              symbol: symbol.symbol,
180
+              symbolId: symbol.id,
181
+              price: data.price,
182
+              bid: data.bid,
183
+              ask: data.ask,
184
+              bidSize: data.bidSize,
185
+              askSize: data.askSize,
186
+              lastUpdated: data.lastUpdated,
187
+              exchange: symbol.exchange,
188
+              instrumentType: symbol.instrumentType
189
+            };
190
+
191
+            // Emit to all clients subscribed to this symbol
192
+            io.to(`symbol:${symbol.symbol}`).emit('livePriceUpdate', eventData);
193
+
194
+            // Also emit to general live price updates
195
+            io.emit('livePriceUpdate', eventData);
196
+          }
197
+        }
148 198
       }
149 199
 
150 200
       res.json({

+ 55 - 1
src/server.js

@@ -1,6 +1,8 @@
1 1
 const app = require('./app');
2 2
 const { testConnection } = require('./config/database');
3 3
 const logger = require('./utils/logger');
4
+const { createServer } = require('http');
5
+const { Server } = require('socket.io');
4 6
 
5 7
 const PORT = process.env.PORT || 3000;
6 8
 
@@ -10,11 +12,63 @@ const startServer = async () => {
10 12
     // Test database connection
11 13
     await testConnection();
12 14
 
15
+    // Create HTTP server
16
+    const server = createServer(app);
17
+
18
+    // Initialize Socket.io
19
+    const io = new Server(server, {
20
+      cors: {
21
+        origin: process.env.CORS_ORIGIN || "*",
22
+        methods: ["GET", "POST"],
23
+        credentials: true
24
+      }
25
+    });
26
+
27
+    // Socket.io connection handling
28
+    io.on('connection', (socket) => {
29
+      logger.info(`Client connected: ${socket.id}`);
30
+
31
+      // Handle subscription to symbols
32
+      socket.on('subscribe', (symbols) => {
33
+        if (Array.isArray(symbols)) {
34
+          symbols.forEach(symbol => {
35
+            socket.join(`symbol:${symbol}`);
36
+            logger.info(`Client ${socket.id} subscribed to ${symbol}`);
37
+          });
38
+        } else {
39
+          socket.join(`symbol:${symbols}`);
40
+          logger.info(`Client ${socket.id} subscribed to ${symbols}`);
41
+        }
42
+      });
43
+
44
+      // Handle unsubscription
45
+      socket.on('unsubscribe', (symbols) => {
46
+        if (Array.isArray(symbols)) {
47
+          symbols.forEach(symbol => {
48
+            socket.leave(`symbol:${symbol}`);
49
+            logger.info(`Client ${socket.id} unsubscribed from ${symbol}`);
50
+          });
51
+        } else {
52
+          socket.leave(`symbol:${symbols}`);
53
+          logger.info(`Client ${socket.id} unsubscribed from ${symbols}`);
54
+        }
55
+      });
56
+
57
+      // Handle disconnect
58
+      socket.on('disconnect', () => {
59
+        logger.info(`Client disconnected: ${socket.id}`);
60
+      });
61
+    });
62
+
63
+    // Make io accessible to routes/controllers
64
+    app.set('io', io);
65
+
13 66
     // Start the server
14
-    const server = app.listen(PORT, () => {
67
+    server.listen(PORT, () => {
15 68
       logger.info(`Market Data Service is running on port ${PORT}`);
16 69
       logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
17 70
       logger.info(`Health check available at: http://localhost:${PORT}/health`);
71
+      logger.info(`WebSocket server ready for connections`);
18 72
     });
19 73
 
20 74
     // Graceful shutdown