Bỏ qua

Ví dụ tích hợp SDK

Các plugin WebView sẽ có nhiều tính năng hơn khi tích hợp phương thức SDK để truy xuất dữ liệu, giao dịch và các thao tác khác mang lại lợi ích cho người dùng.

Ghi chú

Ví dụ trong bài viết này không yêu cầu bất kỳ pipeline hoặc thiết lập nào vì tất cả các phụ thuộc đã được cấu hình để tải trực tiếp từ esm.sh. Đoạn mã này có thể được lưu dưới dạng tệp .html và sau đó tải trực tiếp lên một dịch vụ lưu trữ để triển khai nhanh chóng. Cuối cùng, URL trang web tạo thành có thể được sử dụng để xây dựng một plugin WebView.

Mẹo

Cung cấp đoạn mã ví dụ và tài liệu tham khảo SDK cho một mô hình AI, sau đó hướng dẫn AI chỉnh sửa mã để tạo và triển khai ý tưởng trang web của bạn dành cho plugin WebView.

Mã HTML dưới đây là một ví dụ độc lập về một trang web dành cho plugin WebView. Ứng dụng web này kết nối với máy chủ giao dịch, trao đổi dữ liệu và thực hiện các hoạt động giao dịch trong thời gian thực.

Ví dụ
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>SDK integration example — single-file build-less</title>
  <meta name="viewport" content="width=device-width,initial-scale=1"/>

  <!-- Minimal styles -->
  <style>
    html,body{margin:0;height:100%;font-family:Arial,Helvetica,sans-serif}
    #root{height:100%;display:flex;justify-content:center;align-items:flex-start;
          background:#fff;color:#000;padding:10px;overflow:auto}
    button{padding:4px 8px;margin:2px}
    input,select{padding:2px;margin-left:4px}
    pre{margin:0}
  </style>
</head>

<body>
  <div id="root"></div>

  <script type="module">
    /***  CDN imports  ***/
    import React, { useState, useEffect, useRef, useCallback } from "https://esm.sh/react@19.1.0";
    import { createRoot }                                    from "https://esm.sh/react-dom@19.1.0/client";

    /* SDK + helpers */
    import { createClientAdapter }
      from "https://esm.sh/@spotware-web-team/sdk-external-api@0.0.5";
    import {
      cancelOrder, closePosition, createNewOrder,
      executionEvent, getAccountGroupInformation, getAccountInformation,
      getDealList, getDynamicLeverage, getLightSymbolList, getSymbol,
      handleConfirmEvent, modifyOrder, modifyOrderProtection, quoteEvent,
      registerEvent, ServerInterfaces, subscribeQuotes, unsubscribeQuotes
    } from "https://esm.sh/@spotware-web-team/sdk@0.0.9";
    import { take, tap, catchError }   from "https://esm.sh/rxjs@7.8.2";
    import { createLogger }            from "https://esm.sh/@veksa/logger@1.0.6";

    /*** Example component  ***/
    function Example(){
      /* --- connection / adapters --- */
      const [connected,setConnected] = useState(false);
      const adapter                  = useRef(null);

      /* --- UI state --- */
      const [logs,setLogs]                         = useState([]);
      const [symbolId,          setSymbolId]       = useState(1);
      const [quotesSymbolId,    setQuotesSymbolId] = useState(1);
      const [leverageId,        setLeverageId]     = useState(1);
      const [newOrderSymbolId,  setNewOrderSymbolId]  = useState(1);
      const [newOrderVolume,    setNewOrderVolume] = useState(100000);
      const [modifyOrderId,     setModifyOrderId]  = useState(1);
      const [modifyOrderVolume, setModifyOrderVolume]=useState(100000);
      const [modifyPrice,       setModifyPrice]    = useState(100);
      const [modifyPositionId,  setModifyPositionId]=useState(1);
      const [modifyStopLoss,    setModifyStopLoss] = useState(0.1);
      const [modifyTakeProfit,  setModifyTakeProfit]=useState(0.1);
      const [cancelOrderId,     setCancelOrderId]  = useState(1);
      const [closePositionId,   setClosePositionId]=useState(1);
      const [closePositionVolume,setClosePositionVolume]=useState(100000);

      const log = m => setLogs(p=>[...p, typeof m==="string"?m:JSON.stringify(m,null,2)]);

      /* ---------- connect on mount ---------- */
      useEffect(()=>{
        const logger   = createLogger(location.href.includes("showLogs"));
        adapter.current= createClientAdapter({logger});
        log("🔌 Connecting…");

        handleConfirmEvent(adapter.current,{}).pipe(take(1)).subscribe();

        registerEvent(adapter.current).pipe(
          take(1),
          tap(()=>{
            handleConfirmEvent(adapter.current,{}).pipe(take(1)).subscribe();
            setConnected(true); log("✅ Connected");
          }),
          catchError(()=>{ log("❌ Error host connection"); return []; })
        ).subscribe();
      },[]);

      /* ---------- helper wrappers ---------- */
      const handleAccountInformation   = ()=> getAccountInformation(adapter.current,{}).pipe(take(1), tap(log)).subscribe();
      const handleAccountGroupInformation=()=>getAccountGroupInformation(adapter.current,{}).pipe(take(1),tap(log)).subscribe();
      const handleLightSymbolList      = ()=> getLightSymbolList(adapter.current,{}).pipe(take(1),tap(log)).subscribe();
      const handleSymbol               = ()=> getSymbol(adapter.current,{symbolId:[symbolId]}).pipe(take(1),tap(log)).subscribe();
      const handleSubscribeQuotes      = ()=> subscribeQuotes(adapter.current,{symbolId:[quotesSymbolId]}).pipe(take(1),tap(log)).subscribe();
      const handleUnsubscribeQuotes    = ()=> unsubscribeQuotes(adapter.current,{symbolId:[quotesSymbolId]}).pipe(take(1),tap(log)).subscribe();
      const handleDynamicLeverage      = ()=> getDynamicLeverage(adapter.current,{leverageId}).pipe(take(1),tap(log)).subscribe();
      const handleDealList             = ()=> getDealList(adapter.current,{fromTimestamp:0,toTimestamp:Date.now()}).pipe(take(1),tap(log)).subscribe();
      const handleCreateNewOrder       = ()=> createNewOrder(adapter.current,{
                                            symbolId:newOrderSymbolId,
                                            orderType:ServerInterfaces.ProtoOrderType.MARKET,
                                            tradeSide:ServerInterfaces.ProtoTradeSide.BUY,
                                            volume:newOrderVolume,
                                          }).pipe(tap(log)).subscribe();
      const handleModifyOrder          = ()=> modifyOrder(adapter.current,{
                                            orderId:modifyOrderId,
                                            limitPrice:modifyPrice,
                                            volume:modifyOrderVolume,
                                          }).pipe(tap(log)).subscribe();
      const handleModifyOrderProtection= ()=> modifyOrderProtection(adapter.current,{
                                            positionId:modifyPositionId,
                                            stopLoss:modifyStopLoss,
                                            takeProfit:modifyTakeProfit,
                                          }).pipe(tap(log)).subscribe();
      const handleCancelOrder          = ()=> cancelOrder(adapter.current,{orderId:cancelOrderId}).pipe(tap(log)).subscribe();
      const handleClosePosition        = ()=> closePosition(adapter.current,{
                                            positionId:closePositionId,
                                            volume:closePositionVolume,
                                          }).pipe(tap(log)).subscribe();

      /* ---------- stream quotes / executions ---------- */
      useEffect(()=>{
        if(!connected) return;
        quoteEvent(adapter.current).pipe(tap(log)).subscribe();
        executionEvent(adapter.current).pipe(tap(log)).subscribe();
      },[connected]);

      const e = React.createElement;          // shorthand

      /* first column of controls */
      const controlBlock = e("div",{style:{
          display:"flex",flexDirection:"column",alignItems:"flex-start",
          gap:10,flexWrap:"wrap",marginBottom:10}},
        [
          e("button",{disabled:!connected,onClick:handleAccountInformation},"getAccountInformation"),
          e("button",{disabled:!connected,onClick:handleAccountGroupInformation},"getAccountGroupInformation"),
          e("button",{disabled:!connected,onClick:handleLightSymbolList},"getLightSymbolList"),

          /* getSymbol */
          e("div",null,[
            e("button",{disabled:!connected,onClick:handleSymbol},"getSymbol"),
            " symbolId:",
            e("input",{style:{width:30},value:symbolId,
                      onChange:ev=>setSymbolId(+ev.target.value)}),
          ]),

          /* subscribeQuotes */
          e("div",null,[
            e("button",{disabled:!connected,onClick:handleSubscribeQuotes},"subscribeQuotes"),
            " symbolId:",
            e("input",{style:{width:30},value:quotesSymbolId,
                      onChange:ev=>setQuotesSymbolId(+ev.target.value)}),
          ]),

          /* unsubscribeQuotes */
          e("div",null,[
            e("button",{disabled:!connected,onClick:handleUnsubscribeQuotes},"unsubscribeQuotes"),
            " symbolId:",
            e("input",{style:{width:30},value:quotesSymbolId,
                      onChange:ev=>setQuotesSymbolId(+ev.target.value)}),
          ]),

          /* getDynamicLeverage */
          e("div",null,[
            e("button",{disabled:!connected,onClick:handleDynamicLeverage},"getDynamicLeverage"),
            " leverageId:",
            e("input",{style:{width:30},value:leverageId,
                      onChange:ev=>setLeverageId(+ev.target.value)}),
          ]),

          e("button",{disabled:!connected,onClick:handleDealList},"getDealList"),

          /* createNewOrder */
          e("div",null,[
            e("button",{disabled:!connected,onClick:handleCreateNewOrder},"createNewOrder"),
            " symbolId:",
            e("input",{style:{width:30},value:newOrderSymbolId,
                      onChange:ev=>setNewOrderSymbolId(+ev.target.value)}),
            " volume:",
            e("input",{style:{width:60},value:newOrderVolume,
                      onChange:ev=>setNewOrderVolume(+ev.target.value)}),
          ]),

          /* modifyOrder */
          e("div",null,[
            e("button",{disabled:!connected,onClick:handleModifyOrder},"modifyOrder"),
            " orderId:",
            e("input",{style:{width:30},value:modifyOrderId,
                      onChange:ev=>setModifyOrderId(+ev.target.value)}),
            " volume:",
            e("input",{style:{width:60},value:modifyOrderVolume,
                      onChange:ev=>setModifyOrderVolume(+ev.target.value)}),
            " modify price:",
            e("input",{style:{width:40},value:modifyPrice,
                      onChange:ev=>setModifyPrice(+ev.target.value)}),
          ]),

          /* modifyOrderProtection */
          e("div",null,[
            e("button",{disabled:!connected,onClick:handleModifyOrderProtection},"modifyOrderProtection"),
            " positionId:",
            e("input",{style:{width:30},value:modifyPositionId,
                      onChange:ev=>setModifyPositionId(+ev.target.value)}),
            " stop loss:",
            e("input",{style:{width:40},value:modifyStopLoss,
                      onChange:ev=>setModifyStopLoss(+ev.target.value)}),
            " takeProfit:",
            e("input",{style:{width:40},value:modifyTakeProfit,
                      onChange:ev=>setModifyTakeProfit(+ev.target.value)}),
          ]),

          /* cancelOrder */
          e("div",null,[
            e("button",{disabled:!connected,onClick:handleCancelOrder},"cancelOrder"),
            " orderId:",
            e("input",{style:{width:30},value:cancelOrderId,
                      onChange:ev=>setCancelOrderId(+ev.target.value)}),
          ]),

          /* closePosition */
          e("div",null,[
            e("button",{disabled:!connected,onClick:handleClosePosition},"closePosition"),
            " positionId:",
            e("input",{style:{width:30},value:closePositionId,
                      onChange:ev=>setClosePositionId(+ev.target.value)}),
            " volume:",
            e("input",{style:{width:60},value:closePositionVolume,
                      onChange:ev=>setClosePositionVolume(+ev.target.value)}),
          ]),
        ]);

      /* log viewer */
      const logPane = e("div",{style:{
          height:"100%",overflowY:"scroll",minHeight:200,background:"#eee",
          padding:6,borderRadius:4}},
        logs.map((l,i)=>e("pre",{key:i,style:{marginBottom:10}},l)));

      return e("div",{style:{display:"flex",flexDirection:"column",
                            width:"100%",padding:10,overflow:"auto"}},[
              controlBlock,
              logPane
      ]);
    }

    /***  Mount to DOM  ***/
    createRoot(document.getElementById("root")).render(React.createElement(Example));
  </script>
</body>
</html>

Bảng này phân tích các thao tác điển hình và quan trọng trong ví dụ:

Thao tác Mục đích Ví dụ
Bắt tay Mở WebSocket và hoàn thành chuỗi xác nhận → đăng ký → xác nhận. useEffect đầu tiên
Đồng bộ hóa liên tục Truyền dữ liệu thị trường và các sự kiện khớp lệnh để giao diện người dùng luôn cập nhật. useEffect thứ hai
Gắn kết Chèn giao diện người dùng (React) vào trang hoặc vùng chứa WebView. Lệnh createRoot(…).render(…) ở cuối mã
Hành động SDK Gọi các phương thức SDK, chẳng hạn như getAccountInformation, createNewOrder, modifyOrder, closePosition, v.v., để phản hồi tương tác của người dùng. Các hàm xử lý sự kiện riêng lẻ cho từng nút bên trong component Example

Bắt tay

Khi plugin tải, nó thiết lập kết nối với máy chủ cTrader bằng cách tạo một phiên bản clientAdapter. Bộ chuyển đổi này cho phép giao tiếp hai chiều giữa plugin WebView và nền tảng.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// The first useEffect, which runs only on initial mounting
useEffect(() => {
  // 1. Open the WebSocket connection
  const logger        = createLogger(location.href.includes("showLogs"));
  adapter.current     = createClientAdapter({ logger });
  log("🔌 Connecting…");

  // 2. First handshake message (“confirm”)
  handleConfirmEvent(adapter.current, {})
    .pipe(take(1))
    .subscribe();

  // 3. Wait until the host registers the client
  registerEvent(adapter.current)
    .pipe(
      take(1),
      tap(() => {
        // 4. Second confirmation required by the protocol
        handleConfirmEvent(adapter.current, {})
          .pipe(take(1))
          .subscribe();

        // 5. Fully in-sync. Unlock the UI
        setConnected(true);
        log("✅ Connected");
      }),
      catchError(err => {
        log("❌ Error host connection");
        log(err);
        return [];
      })
    )
    .subscribe();
}, []);        // Empty deps → runs once

Đồng bộ hóa liên tục

Sau khi quá trình bắt tay hoàn tất và plugin được kết nối đầy đủ, nó bắt đầu đồng bộ hóa với máy chủ bằng cách đăng ký hai luồng sự kiện liên tục: báo giá trực tiếp và cập nhật thực thi giao dịch. Điều này đảm bảo plugin nhận được giá cả cập nhật nhất và hoạt động lệnh từ môi trường giao dịch theo thời gian thực.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// The second useEffect, which depends on the `connected` flag
useEffect(() => {
  if (!connected) return;   // do not subscribe until handshake finishes

  // 1. Subscribe to live quotes (price updates)
  quoteEvent(adapter.current)
    .pipe(tap(log))
    .subscribe();

  // 2. Subscribe to trade executions (order fills, cancels, rejections)
  executionEvent(adapter.current)
    .pipe(tap(log))
    .subscribe();
}, [connected]);             // rerun this block only once `connected` becomes true

Gắn kết

Sau khi định nghĩa component Example, plugin sẽ kết xuất giao diện người dùng của nó vào DOM bằng cách sử dụng API createRoot của React. Bước này gắn plugin cả về giao diện lẫn chức năng vào vùng chứa của máy chủ, cho phép người dùng tương tác.

1
2
3
// At the bottom. This runs once at load
createRoot(document.getElementById('root'))
  .render(React.createElement(Example));

Bước gắn kết này hoàn tất giai đoạn thiết lập bằng cách đặt plugin vào giao diện hiển thị và cho phép logic được xác định bên trong Example() bắt đầu chạy, bao gồm xử lý kết nối, liên kết SDK và đăng ký sự kiện.

Hành động SDK

SDK cung cấp các trình bao bọc tiện lợi để yêu cầu thông tin liên quan đến giao dịch, và các lệnh gọi SDK trong mã đều theo cùng một mẫu:

1
2
3
sdkMethod(adapter.current, params)
  .pipe(take(1), tap(log), catchError(handleError))
  .subscribe();

Mỗi hàm này được gắn với một nút, và nút đó chỉ được kích hoạt khi plugin được kết nối. Mỗi lần nhấp vào nút sẽ gọi một phương thức SDK cụ thể, chẳng hạn như lấy dữ liệu tài khoản hoặc tạo một lệnh.

Lấy thông tin tài khoản

1
getAccountInformation(adapter, {}).pipe(take(1), tap(log)).subscribe();

Tạo lệnh thị trường mới

1
2
3
4
5
6
createNewOrder(adapter, {
  symbolId: 1,
  orderType: ServerInterfaces.ProtoOrderType.MARKET,
  tradeSide: ServerInterfaces.ProtoTradeSide.BUY,
  volume: 100000,
}).pipe(tap(log)).subscribe();

Lấy danh sách các ký hiệu có thể giao dịch

1
getLightSymbolList(adapter, {}).pipe(take(1), tap(log)).subscribe();

Đăng ký và hủy đăng ký nhận báo giá

1
2
subscribeQuotes(adapter, { symbolId: [1] }).pipe(take(1), tap(log)).subscribe();
unsubscribeQuotes(adapter, { symbolId: [1] }).pipe(take(1), tap(log)).subscribe();

Ngoài ra, bạn có thể lắng nghe các cập nhật giá và nhận kết quả thực hiện giao dịch bằng cách sử dụng:

1
2
quoteEvent(adapter.current).pipe(tap(log)).subscribe();
executionEvent(adapter.current).pipe(tap(log)).subscribe();

Đầu ra bản ghi và gỡ lỗi (tùy chọn)

Tất cả phản hồi và sự kiện từ SDK được ghi lại theo thời gian thực vào một bảng điều khiển bên cạnh bằng cách sử dụng:

1
const log = m => setLogs(p => [...p, typeof m === "string" ? m : JSON.stringify(m, null, 2)]);

Khu vực đầu ra được hiển thị bằng cách sử dụng:

1
logs.map((l, i) => e("pre", { key: i }, l))

Các bản ghi này cho phép kiểm tra chi tiết các tải trọng JSON và giúp xác nhận liệu một thao tác đã thành công hay thất bại, kèm theo các thông tin chi tiết.