콘텐츠로 이동

SDK 통합 예시

WebView 플러그인은 데이터 검색, 거래 및 사용자에게 도움이 되는 기타 작업을 위해 SDK 메서드를 통합할 때 더 큰 능력을 발휘합니다.

참고

이 문서의 예시는 모든 종속성을 esm.sh에서 직접 로드하도록 구성되어 있으므로 별도의 파이프라인이나 설정이 필요하지 않습니다. 코드는 .html 파일로 저장한 다음 바로 호스팅 서비스에 업로드하여 빠르게 배포할 수 있습니다. 마지막으로, 그 결과로 생성된 웹사이트 URL을 사용하여 WebView 플러그인을 빌드할 수 있습니다.

예시 코드와 SDK 참조를 AI 모델에 제공한 다음, AI가 코드를 편집하여 WebView 플러그인용 웹사이트에 대한 아이디어를 생성 및 제시하도록 유도하세요.

아래의 HTML 코드는 WebView 플러그인용 웹사이트의 완전한 예시입니다. 이 웹 앱은 트레이딩 호스트에 연결하여 데이터를 교환하고 실시간으로 거래 작업을 실행합니다.

예시
  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>

이 표는 예시에서 일반적이고 중요한 작업들을 상세히 설명합니다.

작업 목적 인스턴스
핸드셰이크 WebSocket을 열고 확인 → 등록 → 확인 작업을 순차적으로 완료합니다. 첫 번째 useEffect
지속적인 동기화 UI가 최신 상태를 유지할 수 있도록 시장 데이터와 실행 이벤트를 스트림합니다. 두 번째 useEffect
마운팅 (React) UI를 페이지 또는 WebView 컨테이너에 삽입합니다. 코드 하단의 createRoot(…).render(…) 호출
SDK 작업 사용자 상호 작용에 응답하여 getAccountInformation, createNewOrder, modifyOrder, closePosition 등의 SDK 메서드를 호출합니다. Example 컴포넌트 내부의 개별 버튼 핸들러 함수

핸드셰이크

플러그인이 로드되면 clientAdapter 인스턴스를 생성하여 cTrader 호스트와의 연결을 설정합니다. 이 어댑터는 WebView 플러그인과 플랫폼 간의 양방향 통신을 가능하게 합니다.

 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

지속적인 동기화

핸드셰이크가 완료되고 플러그인이 완전히 연결되면, 실시간 시세와 거래 실행 업데이트라는 두 개의 연속적인 이벤트 스트림에 구독하여 호스트와의 동기화를 시작합니다. 이를 통해 플러그인은 거래 환경으로부터 실시간으로 최신 가격 및 주문 활동을 받을 수 있습니다.

 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

마운팅

Example 컴포넌트를 정의한 후, 플러그인은 React createRoot API를 사용하여 사용자 인터페이스를 DOM에 렌더링합니다. 이 단계는 시각적, 기능적으로 플러그인을 호스트 컨테이너 안에 마운트하여 사용자 상호 작용을 가능하게 합니다.

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

이 마운팅 단계는 플러그인을 가시적 인터페이스에 배치하여 설정 단계를 완료하고, Example() 내부에 정의된 로직(연결 처리, SDK 바인딩, 이벤트 구독 등)이 실행되도록 합니다.

SDK 작업

SDK는 거래 관련 정보를 요청하기 위한 편리한 래퍼를 제공하며, 코드 내의 SDK 호출은 동일한 패턴을 공유합니다.

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

이러한 각 함수는 플러그인이 연결된 후에만 활성화되는 버튼에 연결됩니다. 버튼 클릭이 발생할 때마다 계정 데이터 가져오기 또는 주문 생성과 같은 특정 SDK 메서드를 호출합니다.

계정 정보 가져오기

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

새로운 시장가 주문 생성

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

거래 가능한 심벌 목록 가져오기

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

시세 구독 및 구독 취소

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

또한, 다음을 사용하여 가격 업데이트를 수신하고 거래 실행 결과를 받을 수 있습니다.

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

로깅 및 디버깅 출력 (선택 사항)

모든 SDK 응답 및 이벤트는 다음을 사용하여 실시간으로 사이드 패널에 로그로 기록됩니다.

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

출력 영역은 다음을 사용하여 렌더링됩니다.

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

이 로그를 통해 JSON 페이로드를 검사하고 작업이 성공했는지 또는 실패했는지를 세부 정보와 함께 확인할 수 있습니다.