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.
- programgarden-0.1.0/PKG-INFO +27 -0
- programgarden-0.1.0/README.md +1 -0
- programgarden-0.1.0/programgarden/__init__.py +135 -0
- programgarden-0.1.0/programgarden/buysell_executor.py +347 -0
- programgarden-0.1.0/programgarden/client.py +162 -0
- programgarden-0.1.0/programgarden/condition_executor.py +348 -0
- programgarden-0.1.0/programgarden/pg_listener.py +214 -0
- programgarden-0.1.0/programgarden/plugin_resolver.py +206 -0
- programgarden-0.1.0/programgarden/real_order_executor.py +344 -0
- programgarden-0.1.0/programgarden/symbols_provider.py +123 -0
- programgarden-0.1.0/programgarden/system_executor.py +360 -0
- programgarden-0.1.0/programgarden/system_keys.py +112 -0
- programgarden-0.1.0/pyproject.toml +36 -0
|
@@ -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)
|