programgarden 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.3
2
+ Name: programgarden
3
+ Version: 0.1.0
4
+ Summary: 자동화매매 오픈소스의 실행, 로그, 환경 등의 핵심이되는 오픈소스
5
+ Author: 프로그램동산
6
+ Author-email: coding@programgarden.com
7
+ Requires-Python: >=3.9
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Dist: art (>=6.5,<7.0)
15
+ Requires-Dist: croniter (>=6.0.0,<7.0.0)
16
+ Requires-Dist: flask (>=3.1.2,<4.0.0)
17
+ Requires-Dist: jsoneditor (>=1.6.0,<2.0.0)
18
+ Requires-Dist: programgarden-community (>=0.1.0,<0.2.0)
19
+ Requires-Dist: programgarden-core (>=0.1.0,<0.2.0)
20
+ Requires-Dist: programgarden-finance (>=0.1.0,<0.2.0)
21
+ Requires-Dist: pydantic (>=2.11.7,<3.0.0)
22
+ Requires-Dist: pyqt5 (>=5.15.11,<6.0.0)
23
+ Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
24
+ Requires-Dist: qscintilla (>=2.14.1,<3.0.0)
25
+ Description-Content-Type: text/markdown
26
+
27
+ programgarden
@@ -0,0 +1 @@
1
+ programgarden
@@ -0,0 +1,135 @@
1
+ """
2
+ 클라이언트 기능 모듈
3
+
4
+ LS OpenAPI 클라이언트가 사용하는 모듈입니다.
5
+ """
6
+
7
+ from programgarden.system_executor import SystemExecutor
8
+ from programgarden_core import exceptions
9
+ from .client import Programgarden
10
+ from .pg_listener import (
11
+ PGListener,
12
+ )
13
+ from programgarden_finance import (
14
+ ls,
15
+ LS,
16
+ oauth,
17
+ overseas_stock,
18
+ overseas_futureoption,
19
+
20
+ COSAQ00102,
21
+ COSAQ01400,
22
+ COSOQ00201,
23
+ COSOQ02701,
24
+ g3103,
25
+ g3202,
26
+ g3203,
27
+ g3204,
28
+ g3101,
29
+ g3102,
30
+ g3104,
31
+ g3106,
32
+ g3190,
33
+
34
+ o3101,
35
+ o3104,
36
+ o3105,
37
+ o3106,
38
+ o3107,
39
+ o3116,
40
+ o3121,
41
+ o3123,
42
+ o3125,
43
+ o3126,
44
+ o3127,
45
+ o3128,
46
+ o3136,
47
+ o3137,
48
+
49
+ COSAT00301,
50
+ COSAT00311,
51
+ COSMT00300,
52
+ COSAT00400,
53
+
54
+ CIDBQ01400,
55
+ CIDBQ01500,
56
+ CIDBQ01800,
57
+ CIDBQ02400,
58
+ CIDBQ03000,
59
+ CIDBQ05300,
60
+ CIDEQ00800,
61
+
62
+ o3103,
63
+ o3108,
64
+ o3117,
65
+ o3139,
66
+
67
+ CIDBT00100,
68
+ CIDBT00900,
69
+ CIDBT01000
70
+ )
71
+
72
+ __all__ = [
73
+ SystemExecutor, # SystemExecutor 클래스는 시스템 실행을 담당합니다.
74
+ Programgarden,
75
+ ls,
76
+ LS,
77
+ oauth,
78
+ exceptions,
79
+
80
+ PGListener,
81
+
82
+ overseas_stock,
83
+ overseas_futureoption,
84
+
85
+ COSAQ00102,
86
+ COSAQ01400,
87
+ COSOQ00201,
88
+ COSOQ02701,
89
+ g3103,
90
+ g3202,
91
+ g3203,
92
+ g3204,
93
+ g3101,
94
+ g3102,
95
+ g3104,
96
+ g3106,
97
+ g3190,
98
+
99
+ COSAT00301,
100
+ COSAT00311,
101
+ COSMT00300,
102
+ COSAT00400,
103
+
104
+ o3101,
105
+ o3104,
106
+ o3105,
107
+ o3106,
108
+ o3107,
109
+ o3116,
110
+ o3121,
111
+ o3123,
112
+ o3125,
113
+ o3126,
114
+ o3127,
115
+ o3128,
116
+ o3136,
117
+ o3137,
118
+
119
+ CIDBQ01400,
120
+ CIDBQ01500,
121
+ CIDBQ01800,
122
+ CIDBQ02400,
123
+ CIDBQ03000,
124
+ CIDBQ05300,
125
+ CIDEQ00800,
126
+
127
+ o3103,
128
+ o3108,
129
+ o3117,
130
+ o3139,
131
+
132
+ CIDBT00100,
133
+ CIDBT00900,
134
+ CIDBT01000
135
+ ]
@@ -0,0 +1,347 @@
1
+ """
2
+ This module provides the BuyExecutor class which is responsible for
3
+ resolving and executing external buy/sell plugin classes (conditions).
4
+
5
+ The executor reads a system configuration, resolves the plugin by its
6
+ identifier, instantiates it with configured parameters, and runs its
7
+ "execute" method. Results (symbols to act on) are returned to the
8
+ caller and also logged.
9
+
10
+ The implementations here are intentionally small: the executor focuses
11
+ on orchestration (resolve -> instantiate -> set context -> execute)
12
+ and leaves trading logic to plugin classes that must subclass
13
+ `BaseBuyOverseasStock` from `programgarden_core`.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from typing import TYPE_CHECKING, List, Optional, TypedDict, Union
18
+ from zoneinfo import ZoneInfo
19
+ from programgarden_core import (
20
+ SystemType, SymbolInfo,
21
+ BaseBuyOverseasStockResponseType, BaseSellOverseasStockResponseType,
22
+ pg_logger, BaseBuyOverseasStock, exceptions, NewBuyTradeType, HeldSymbol,
23
+ NonTradedSymbol, NewSellTradeType, BaseSellOverseasStock,
24
+ OrderCategoryType
25
+ )
26
+ from programgarden_finance import LS, COSAT00301, COSOQ00201, COSAQ00102, COSOQ02701
27
+
28
+ from programgarden.pg_listener import pg_listener
29
+ from programgarden.real_order_executor import RealOrderExecutor
30
+ from datetime import datetime
31
+
32
+ if TYPE_CHECKING:
33
+ from .plugin_resolver import PluginResolver
34
+
35
+
36
+ class DpsTyped(TypedDict):
37
+ fcurr_dps: float
38
+ fcurr_ord_able_amt: float
39
+
40
+
41
+ class BuySellExecutor:
42
+ """Orchestrates execution of buy/sell condition plugins.
43
+
44
+ The executor requires a `PluginResolver` which maps condition
45
+ identifiers to concrete classes. It does not implement trading
46
+ strategies itself; instead it prepares and runs plugin instances
47
+ and returns whatever those plugins produce.
48
+
49
+ Contract (high level):
50
+ - Input: a `system` config (dict-like `SystemType`) and a list of
51
+ `SymbolInfo` items describing available symbols.
52
+ - Output: a list of plugin execution responses (or None on error).
53
+ - Error modes: missing plugin, incorrect plugin type, runtime
54
+ exceptions inside plugin code. Errors are logged and result in
55
+ a None return value from the internal executor.
56
+ """
57
+
58
+ def __init__(self, plugin_resolver: PluginResolver):
59
+ # PluginResolver instance used to look up condition classes by id
60
+ self.plugin_resolver = plugin_resolver
61
+ self.real_order_executor = RealOrderExecutor()
62
+
63
+ async def new_buy_execute(
64
+ self,
65
+ system: SystemType,
66
+ symbols_from_strategy: List[SymbolInfo],
67
+ new_buy: NewBuyTradeType,
68
+ order_id: str,
69
+ ) -> None:
70
+ """
71
+ Public entrypoint to perform buy execution for a system.
72
+ """
73
+
74
+ # 예수금 세팅
75
+ available_balance = float(new_buy.get("available_balance", 0.0))
76
+ dps: DpsTyped = {
77
+ "fcurr_dps": available_balance,
78
+ "fcurr_ord_able_amt": available_balance,
79
+ }
80
+ is_ls = system.get("securities", {}).get("company", None) == "ls"
81
+
82
+ if available_balance == 0.0 and is_ls:
83
+ # 예수금 가져오기
84
+ cosoq02701 = await LS.get_instance().overseas_stock().accno().cosoq02701(
85
+ body=COSOQ02701.COSOQ02701InBlock1(
86
+ RecCnt=1,
87
+ CrcyCode="USD",
88
+ ),
89
+ ).req_async()
90
+
91
+ dps["fcurr_dps"] = cosoq02701.block3[0].FcurrDps
92
+ dps["fcurr_ord_able_amt"] = cosoq02701.block3[0].FcurrOrdAbleAmt
93
+
94
+ # 필터링, 보유, 미체결 종목들 가져오기
95
+ filtered_symbols, held_symbols, non_trade_symbols = await self._block_duplicate_symbols(system, symbols_from_strategy)
96
+
97
+ # 종목 보유중이면 막기
98
+ if new_buy.get("block_duplicate_trade", True):
99
+ symbols_from_strategy[:] = filtered_symbols
100
+
101
+ if not symbols_from_strategy:
102
+ return
103
+
104
+ purchase_symbols, community_instance = await self.plugin_resolver.resolve_buysell_community(
105
+ system_id=system.get("settings", {}).get("system_id", None),
106
+ trade=new_buy,
107
+ symbols=symbols_from_strategy,
108
+ held_symbols=held_symbols,
109
+ non_trade_symbols=non_trade_symbols,
110
+ dps=dps,
111
+ )
112
+
113
+ if not purchase_symbols:
114
+ return
115
+
116
+ for symbol in purchase_symbols:
117
+ if not symbol.get("success"):
118
+ continue
119
+
120
+ # 주문 함수 구성
121
+ result = await self._build_order_function(
122
+ system=system,
123
+ trade_type="submitted_new_buy",
124
+ symbol=symbol
125
+ )
126
+
127
+ ord_no = None
128
+ if result is not None:
129
+ block2 = getattr(result, "block2", None)
130
+ ord_val = getattr(block2, "OrdNo", None) if block2 is not None else None
131
+ ord_no = str(ord_val) if ord_val is not None else None
132
+
133
+ await self.real_order_executor.send_data_community_instance(
134
+ ordNo=ord_no,
135
+ community_instance=community_instance
136
+ )
137
+
138
+ pg_logger.info(f"🟢 New buy order executed for order '{order_id}'")
139
+
140
+ async def _block_duplicate_symbols(
141
+ self,
142
+ system: SystemType,
143
+ symbols_from_strategy: List[SymbolInfo],
144
+ ):
145
+ """
146
+ Filter out only the stocks that are not held
147
+
148
+ Returns로는 중복 여부 필터링한 종목들과, 보유잔고 종목들과 미체결 종목들이 반환된다.
149
+ """
150
+
151
+ held_symbols: List[HeldSymbol] = []
152
+ non_trade_symbols: List[NonTradedSymbol] = []
153
+
154
+ company = system.get("securities", {}).get("company", "")
155
+ product = system.get("securities", {}).get("product", [])
156
+ if company == "ls" and product == "overseas_stock":
157
+ ls = LS.get_instance()
158
+ if not ls.is_logged_in():
159
+ await ls.async_login(
160
+ appkey=system.get("securities", {}).get("appkey", None),
161
+ appsecretkey=system.get("securities", {}).get("appsecretkey", None)
162
+ )
163
+
164
+ # 보유잔고에서 확인하기
165
+ acc_result = await ls.overseas_stock().accno().cosoq00201(
166
+ body=COSOQ00201.COSOQ00201InBlock1(
167
+ # BaseDt=datetime.now(ZoneInfo("America/New_York")).strftime("%Y%m%d")
168
+ )
169
+ ).req_async()
170
+
171
+ held_isus = set()
172
+ for blk in acc_result.block4:
173
+ shtn_isu_no = blk.ShtnIsuNo
174
+ if shtn_isu_no is not None:
175
+ held_isus.add(str(shtn_isu_no).strip())
176
+
177
+ held_symbols.append(
178
+ HeldSymbol(
179
+ CrcyCode=blk.CrcyCode,
180
+ ShtnIsuNo=shtn_isu_no,
181
+ AstkBalQty=blk.AstkBalQty,
182
+ AstkSellAbleQty=blk.AstkSellAbleQty,
183
+ PnlRat=blk.PnlRat,
184
+ BaseXchrat=blk.BaseXchrat,
185
+ PchsAmt=blk.PchsAmt,
186
+ FcurrMktCode=blk.FcurrMktCode
187
+ )
188
+ )
189
+
190
+ # symbols_from_strategy에서
191
+ exchcds: set[str] = set()
192
+ for symbol in symbols_from_strategy:
193
+ exchcds.add(symbol.get("exchcd"))
194
+
195
+ for exchcd in exchcds:
196
+ # 미체결에서도 확인하기
197
+ not_acc_result = await ls.overseas_stock().accno().cosaq00102(
198
+ body=COSAQ00102.COSAQ00102InBlock1(
199
+ QryTpCode="1",
200
+ BkseqTpCode="1",
201
+ OrdMktCode=exchcd,
202
+ BnsTpCode="2",
203
+ SrtOrdNo="999999999",
204
+ OrdDt=datetime.now(ZoneInfo("America/New_York")).strftime("%Y%m%d"),
205
+ ExecYn="2",
206
+ CrcyCode="USD",
207
+ ThdayBnsAppYn="0",
208
+ LoanBalHldYn="0"
209
+ )
210
+ ).req_async()
211
+
212
+ if not_acc_result.block3:
213
+ for blk in not_acc_result.block3:
214
+ isu_no = blk.IsuNo
215
+ if isu_no is not None:
216
+ held_isus.add(str(isu_no).strip())
217
+
218
+ non_trade_symbols.append(
219
+ NonTradedSymbol(
220
+ OrdTime=blk.OrdTime,
221
+ OrdNo=blk.OrdNo,
222
+ OrgOrdNo=blk.OrgOrdNo,
223
+ ShtnIsuNo=blk.ShtnIsuNo,
224
+ MrcAbleQty=blk.MrcAbleQty,
225
+ OrdQty=blk.OrdQty,
226
+ OvrsOrdPrc=blk.OvrsOrdPrc,
227
+ OrdprcPtnCode=blk.OrdprcPtnCode,
228
+ OrdPtnCode=blk.OrdPtnCode,
229
+ MrcTpCode=blk.MrcTpCode,
230
+ OrdMktCode=blk.OrdMktCode,
231
+ UnercQty=blk.UnercQty,
232
+ CnfQty=blk.CnfQty,
233
+ CrcyCode=blk.CrcyCode,
234
+ RegMktCode=blk.RegMktCode,
235
+ IsuNo=blk.IsuNo,
236
+ BnsTpCode=blk.BnsTpCode
237
+ )
238
+ )
239
+
240
+ if held_isus:
241
+ filtered = []
242
+ for m_symbol in symbols_from_strategy:
243
+ m_isu_no = m_symbol.get("symbol")
244
+
245
+ if m_isu_no is None or str(m_isu_no).strip() not in held_isus:
246
+ filtered.append(m_symbol)
247
+ return filtered, held_symbols, non_trade_symbols
248
+
249
+ return [], held_symbols, non_trade_symbols
250
+
251
+ async def new_sell_execute(
252
+ self,
253
+ system: SystemType,
254
+ symbols_from_strategy: List[SymbolInfo],
255
+ new_sell: NewSellTradeType,
256
+ order_id: str,
257
+ ) -> Optional[Union[BaseBuyOverseasStock, BaseSellOverseasStock]]:
258
+ """
259
+ Public entrypoint to perform sell execution for a system.
260
+ """
261
+
262
+ filtered_symbols, held_symbols, non_trade_symbols = await self._block_duplicate_symbols(system, symbols_from_strategy)
263
+
264
+ symbols, community_instance = await self.plugin_resolver.resolve_buysell_community(
265
+ trade=new_sell,
266
+ symbols=symbols_from_strategy,
267
+ held_symbols=held_symbols,
268
+ non_trade_symbols=non_trade_symbols,
269
+ )
270
+
271
+ if not symbols:
272
+ pg_logger.warning("No symbols found for sell strategy.")
273
+
274
+ return
275
+
276
+ for symbol in symbols:
277
+ if not symbol.get("success"):
278
+ continue
279
+
280
+ result = await self._build_order_function(
281
+ system=system,
282
+ trade_type="submitted_new_sell",
283
+ symbol=symbol
284
+ )
285
+
286
+ await self.real_order_executor.send_data_community_instance(
287
+ ordNo=str(result.block2.OrdNo),
288
+ community_instance=community_instance
289
+ )
290
+
291
+ if result.error_msg:
292
+ pg_logger.error(f"Order placement failed: {result.error_msg}")
293
+
294
+ continue
295
+
296
+ pg_logger.info(f"🟢 New buy order executed for order '{order_id}'")
297
+
298
+ async def _build_order_function(
299
+ self,
300
+ system: SystemType,
301
+ trade_type: OrderCategoryType,
302
+ symbol: Union[BaseBuyOverseasStockResponseType, BaseSellOverseasStockResponseType]
303
+ ):
304
+ """
305
+ Function that performs the actual order placement.
306
+ """
307
+ company = system.get("securities", {}).get("company", None)
308
+ product = system.get("securities", {}).get("product", None)
309
+
310
+ if company is None or not product:
311
+ raise exceptions.NotExistCompanyException(
312
+ message="No securities company or product configured in system."
313
+ )
314
+
315
+ if company == "ls":
316
+
317
+ ls = LS.get_instance()
318
+
319
+ if product == "overseas_stock":
320
+ # unify buy/sell order placement
321
+ ord_ptn = "02" if trade_type == "submitted_new_buy" else "01"
322
+ result: COSAT00301.COSAT00301Response = await ls.overseas_stock().order().cosat00301(
323
+ body=COSAT00301.COSAT00301InBlock1(
324
+ OrdPtnCode=ord_ptn,
325
+ OrgOrdNo=None,
326
+ OrdMktCode=symbol.get("ord_mkt_code"),
327
+ IsuNo=symbol.get("isu_no"),
328
+ OrdQty=symbol.get("ord_qty"),
329
+ OvrsOrdPrc=symbol.get("ovrs_ord_prc"),
330
+ OrdprcPtnCode=symbol.get("ordprc_ptn_code"),
331
+ )
332
+ ).req_async()
333
+
334
+ pg_listener.emit_real_order({
335
+ "order_type": trade_type,
336
+ "message": result.rsp_msg,
337
+ "symbol": symbol,
338
+ "response": result,
339
+ })
340
+
341
+ if result.error_msg:
342
+ pg_logger.error(f"Order placement failed: {result.error_msg}")
343
+ raise exceptions.OrderException(
344
+ message=f"Order placement failed: {result.error_msg}"
345
+ )
346
+
347
+ return result
@@ -0,0 +1,162 @@
1
+
2
+ import asyncio
3
+ import logging
4
+ import threading
5
+ from typing import Callable
6
+ from programgarden_core import pg_log, pg_log_disable, pg_logger
7
+ from programgarden_core.bases import SystemType
8
+ from programgarden_finance import LS
9
+ from programgarden_core import EnforceKoreanAliasMeta
10
+ from programgarden_core.exceptions import LoginException
11
+ from programgarden import SystemExecutor
12
+ from programgarden.pg_listener import StrategyPayload, RealOrderPayload, pg_listener
13
+ from .system_keys import exist_system_keys_error
14
+ from art import tprint
15
+
16
+
17
+ def my_processor(event_type: str, data: dict):
18
+ print("global handler:", event_type, data)
19
+
20
+
21
+ class Programgarden(metaclass=EnforceKoreanAliasMeta):
22
+ """
23
+ Programgarden DSL Client for running trading systems.
24
+ """
25
+
26
+ def __init__(self):
27
+
28
+ # 내부 상태
29
+ self._lock = threading.RLock()
30
+
31
+ # lazy init: SystemExecutor는 실제로 필요할 때 생성한다.
32
+ self._executor = None
33
+ self._executor_lock = threading.RLock()
34
+
35
+ # 비동기 실행 태스크 핸들 (이벤트 루프 내에서 중복 실행 방지용)
36
+ self._task = None
37
+
38
+ @property
39
+ def executor(self):
40
+ """
41
+ Lazily create and return the `SystemExecutor` instance.
42
+
43
+ The executor is created on first access. Double-checked locking is
44
+ used to avoid creating multiple executors in concurrent access
45
+ scenarios.
46
+
47
+ Returns:
48
+ SystemExecutor: the executor instance used to run strategies.
49
+ """
50
+ if getattr(self, "_executor", None) is None:
51
+ with self._executor_lock:
52
+ if getattr(self, "_executor", None) is None:
53
+ self._executor = SystemExecutor()
54
+ return self._executor
55
+
56
+ def run(
57
+ self,
58
+ system: SystemType
59
+ ):
60
+ """
61
+ Run the system - continuous execution
62
+ This method starts the system and, if an event loop is already running,
63
+ runs it as a background task. If no event loop is running, it uses
64
+ asyncio.run() to execute the system.
65
+
66
+ Args:
67
+ system (SystemType): The system data object to run.
68
+ """
69
+
70
+ tprint("""
71
+ Program Garden
72
+ x
73
+ LS Securities
74
+ """, font="tarty1")
75
+
76
+ if system:
77
+ self._check_debug(system)
78
+
79
+ try:
80
+ exist_system_keys_error(system)
81
+ asyncio.get_running_loop()
82
+
83
+ if self._task is not None and not self._task.done():
84
+ pg_logger.info("A task is already running; returning the existing task.")
85
+ return self._task
86
+
87
+ task = asyncio.create_task(self._execute(system))
88
+ self._task = task
89
+
90
+ return task
91
+
92
+ except RuntimeError:
93
+ return asyncio.run(self._execute(system))
94
+
95
+ finally:
96
+ pg_logger.error("The program has terminated.")
97
+ pg_listener.stop()
98
+
99
+ def _check_debug(self, system: SystemType):
100
+ """Check debug mode setting and set the logging level"""
101
+
102
+ debug = system.get("settings", {}).get("debug", None)
103
+ if debug == "DEBUG":
104
+ pg_log(logging.DEBUG)
105
+ elif debug == "INFO":
106
+ pg_log(logging.INFO)
107
+ elif debug == "WARNING":
108
+ pg_log(logging.WARNING)
109
+ elif debug == "ERROR":
110
+ pg_log(logging.ERROR)
111
+ elif debug == "CRITICAL":
112
+ pg_log(logging.CRITICAL)
113
+ else:
114
+ pg_log_disable()
115
+
116
+ async def _execute(self, system: SystemType):
117
+ """
118
+ Execute the trading strategy.
119
+ """
120
+ try:
121
+ securities = system.get("securities", {})
122
+ company = securities.get("company", None)
123
+ if company == "ls":
124
+ ls = LS.get_instance()
125
+
126
+ if not ls.is_logged_in():
127
+ login_result = await ls.async_login(
128
+ appkey=securities.get("appkey"),
129
+ appsecretkey=securities.get("appsecretkey")
130
+ )
131
+ if not login_result:
132
+ raise LoginException(
133
+ message="LS 증권 로그인에 실패했습니다. 로그인 정보를 확인하세요."
134
+ )
135
+
136
+ await self.executor.execute_system(system)
137
+
138
+ while self.executor.running:
139
+ await asyncio.sleep(1)
140
+
141
+ finally:
142
+ self.on_strategies_message(
143
+ {
144
+ "event": "system_stopped",
145
+ "message": "시스템이 종료되었습니다."
146
+ }
147
+ )
148
+ await self.stop()
149
+ # 실행 종료 후 태스크 핸들 초기화
150
+ self._task = None
151
+
152
+ async def stop(self):
153
+ await self.executor.stop()
154
+ pg_logger.debug("The program has been stopped.")
155
+
156
+ def on_strategies_message(self, callback: Callable[[StrategyPayload], None]) -> None:
157
+ """실시간 이벤트 수신 콜백 등록"""
158
+ pg_listener.set_strategies_handler(callback)
159
+
160
+ def on_real_order_message(self, callback: Callable[[RealOrderPayload], None]) -> None:
161
+ """실시간 주문 이벤트 수신 콜백 등록"""
162
+ pg_listener.set_real_order_handler(callback)