-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathkis_order_engine.py
More file actions
483 lines (385 loc) Β· 27.6 KB
/
Copy pathkis_order_engine.py
File metadata and controls
483 lines (385 loc) Β· 27.6 KB
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
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# ==========================================================
# FILE: kis_order_engine.py
# ==========================================================
# π¨ MODIFIED: [νμ¬λ ν¨ν΄ 3λ¨κ³] μ£Όλ¬Έ μ μ‘, μ·¨μ, 미체결 λ° μμ₯ μ μ΄ λλ©μΈ λΆλ¦¬
# π¨ MODIFIED: [Thundering Herd μꡬ μκ°] ννΈνλ time.sleep(0.06) λμ§ μ½λλ₯Ό μ λ©΄ μμ νκ³ , λ΄λΆ _api_request μ GlobalThrottle μ€μ ν΅μ λ‘ 100% μμνμ¬ μ΄λ²€νΈ 루ν κ΅μ°© μν μλ²½ λ°©μ΄.
# π¨ MODIFIED: [μ ν μμ λ½μ¨] MarketDataProviderλ₯Ό μμνμ¬ API ν΄λΌμ΄μΈνΈμ μμΈ μ°μ° κΈ°λ₯κΉμ§ ν ν¨ν€μ§ μμ
# π¨ MODIFIED: [Case 18] λ‘컬 μμ½ μ€λ
μ· μ λ©΄ νκΈ° λ° KIS μμ₯ μ§μ μ°λ(get_reservation_orders) κΈ°λ° μ·¨μ μ§ν μ μ§
# π¨ MODIFIED: [TypeError λΆκ΄΄ κΆκ·Ή μμ ] KIS μλ²μμ msg1μ΄ NoneμΌλ‘ λ°νλ λ λ°μνλ λ¬Έμμ΄ λ§€μΉ(in) μ¦μ¬ λ²κ·Έ μλ²½ μ°¨λ¨
# π¨ MODIFIED: [νμ΄μ§ ν©νΈ μμ ] get_execution_history λ΄ μ°μ μ‘°ν(Pagination) ν ν° λ―Έκ°±μ μΌλ‘ μΈν 체결 λ΄μ μ¦λ° λ²κ·Έ μλ²½ μμ
# π¨ MODIFIED: [μ΄λ²€νΈ 루ν κ΅μ°© λ°©μ΄] cancel_all_orders_safe λ΄λΆμ κ³Όλν time.sleep(5)λ₯Ό 1.0μ΄λ‘ λ¨μΆνμ¬ Caller νμμμ(10μ΄) νλ° μμ² μ°¨λ¨
# π¨ VERIFIED: [Case 36 μ λ λ°©μ΄λ§ κ²°μ] MOC(μμ₯κ° λ§€λ) μ£Όλ¬Έ 리μ μ νμ¬κ° -5% μ΅μ 리 μ§μ κ°(LIMIT) λ€ν μλ ν΄λ°± 100% ν©νΈ κ°λ
# π¨ MODIFIED: [μ£Όλ¬Έκ°λ₯κΈμ‘ μμ° ν©νΈ 볡ꡬ] KIS APIκ° νΉμ μκ°λμ μμκΈμ 0.0μΌλ‘ λ°ννλ κ³ μ§μ κ²°ν¨μ μ°ννκΈ° μν΄, μ€λ¦¬μ§λ μ리μ μμ°(Reverse Calc) 곡μ(μΈνμμκΈ+λ§€λμ μ°-λ§€μμ μ°)μ 100% λ‘€λ°± μλ£.
# π¨ MODIFIED: [Case 36 μ λ λ°©μ΄λ§ ν©νΈ κ΅μ ] λ¨μ -5% νν₯ λ‘μ§ μκ° λ° λ§€μ 1νΈκ° ν©νΈ μ€μ μκ²©λ§ κ²°μ μλ£.
# ==========================================================
import time
import datetime
import math
import logging
from zoneinfo import ZoneInfo
from market_data_provider import MarketDataProvider
class KisOrderEngine(MarketDataProvider):
def get_account_balance(self):
""" π¨ [Case 03 μ€μ] API μκ³ μλ΅ μ€λ³΅ ν©μ° μ λ λ°©μ΄ λ½μ¨ """
cash = 0.0
holdings = {}
api_success = False
params = {"CANO": self.cano, "ACNT_PRDT_CD": self.acnt_prdt_cd, "WCRC_FRCR_DVSN_CD": "02", "NATN_CD": "840", "TR_MKET_CD": "00", "INQR_DVSN_CD": "00"}
res = self._call_api("CTRP6504R", "/uapi/overseas-stock/v1/trading/inquire-present-balance", "GET", params=params)
if res.get('rt_cd') == '0':
api_success = True
# π¨ MODIFIED: [Null-Pointer λ°©μ΄] output2 κ²°μΈ‘ μ λΉ λμ
λλ¦¬λ‘ ν΄λ°±
o2 = res.get('output2') or {}
if isinstance(o2, list): o2 = o2[0] if len(o2) > 0 else {}
if not isinstance(o2, dict): o2 = {}
# π¨ MODIFIED: [μ£Όλ¬Έκ°λ₯κΈμ‘ μμ° ν©νΈ 볡ꡬ] KIS API μ€λ₯λ₯Ό μ°ννλ 100% μμ κ³μ°λ§ λ‘€λ°±
dncl_amt = self._safe_float(o2.get('frcr_dncl_amt_2', 0))
sll_amt = self._safe_float(o2.get('frcr_sll_amt_smtl', 0))
buy_amt = self._safe_float(o2.get('frcr_buy_amt_smtl', 0))
raw_bp = dncl_amt + sll_amt - buy_amt
cash = max(0.0, math.floor((raw_bp * 0.9945) * 100) / 100.0)
target_excgs = ["NASD", "AMEX", "NYSE"]
for excg in target_excgs:
fk200, nk200 = "", ""
for attempt in range(20):
# π¨ MODIFIED: [Thundering Herd μꡬ μκ°] ννΈνλ sleep μκ° (GlobalThrottle μμ)
params_hold = {"CANO": self.cano, "ACNT_PRDT_CD": self.acnt_prdt_cd, "OVRS_EXCG_CD": excg, "TR_CRCY_CD": "USD", "CTX_AREA_FK200": fk200, "CTX_AREA_NK200": nk200}
headers = self._get_header("TTTS3012R")
# π¨ MODIFIED: [νμ΄μ§ ν©νΈ μμ ] λ€μ νμ΄μ§ μμ² μ "tr_cont": "N" κ°μ μ£Όμ
if fk200 or nk200: headers["tr_cont"] = "N"
url = f"{self.base_url}/uapi/overseas-stock/v1/trading/inquire-balance"
res_hold, resp_json = self._api_request("GET", url, headers, params=params_hold)
if res_hold and resp_json.get('rt_cd') == '0':
api_success = True
if cash <= 0:
# π¨ MODIFIED: [Null-Pointer λ°©μ΄] output2 κ²°μΈ‘ μ λΉ λμ
λλ¦¬λ‘ ν΄λ°±
o2 = resp_json.get('output2') or {}
if isinstance(o2, list): o2 = o2[0] if len(o2) > 0 else {}
if not isinstance(o2, dict): o2 = {}
new_cash = self._safe_float(o2.get('ovrs_ord_psbl_amt', 0))
if new_cash > cash: cash = new_cash
for item in (resp_json.get('output1') or []):
if not isinstance(item, dict): continue
ticker = item.get('ovrs_pdno')
if not ticker: continue
qty = int(self._safe_float(item.get('ovrs_cblc_qty', 0)))
ord_psbl_qty = int(self._safe_float(item.get('ord_psbl_qty', 0)))
avg = self._safe_float(item.get('pchs_avg_pric', 0))
if qty > 0 and ord_psbl_qty == 0: ord_psbl_qty = qty
if qty > 0:
if ticker not in holdings:
holdings[ticker] = {'qty': qty, 'ord_psbl_qty': ord_psbl_qty, 'avg': avg}
else:
# π¨ MODIFIED: [Case 03] μ λ Ή μ€λ³΅ ν©μ° λμ 무μ (μꡬ μκ°)
continue
tr_cont = res_hold.headers.get('tr_cont', '') if hasattr(res_hold, 'headers') else ''
fk200 = str(resp_json.get('ctx_area_fk200', '') or '').strip()
nk200 = str(resp_json.get('ctx_area_nk200', '') or '').strip()
if tr_cont in ['M', 'F'] and nk200:
continue
else: break
else: break
if api_success: return cash, holdings
else: return cash, None
def get_unfilled_orders_detail(self, ticker):
excg_cd = self._get_exchange_code(ticker, target_api="ORDER")
valid_orders = []
fk200, nk200 = "", ""
for attempt in range(10):
# π¨ MODIFIED: [Thundering Herd μꡬ μκ°] ννΈνλ sleep μκ° (GlobalThrottle μμ)
params = {"CANO": self.cano, "ACNT_PRDT_CD": self.acnt_prdt_cd, "OVRS_EXCG_CD": excg_cd, "SORT_SQN": "DS", "CTX_AREA_FK200": fk200, "CTX_AREA_NK200": nk200}
headers = self._get_header("TTTS3018R")
# π¨ MODIFIED: [νμ΄μ§ ν©νΈ μμ ] λ€μ νμ΄μ§ μμ² μ "tr_cont": "N" κ°μ μ£Όμ
if fk200 or nk200: headers["tr_cont"] = "N"
res, resp_json = self._api_request("GET", f"{self.base_url}/uapi/overseas-stock/v1/trading/inquire-nccs", headers, params=params)
if res and resp_json.get('rt_cd') == '0':
# π¨ MODIFIED: [Iterable λΆκ΄΄ λ°©μ΄] None μ μ
μ []λ‘ λ¨λ½ νκ°
output = resp_json.get('output') or []
if isinstance(output, dict): output = [output]
if not isinstance(output, list): output = []
valid_orders.extend([item for item in output if isinstance(item, dict) and item.get('pdno') == ticker])
tr_cont = res.headers.get('tr_cont', '') if hasattr(res, 'headers') else ''
fk200 = str(resp_json.get('ctx_area_fk200', '') or '').strip()
nk200 = str(resp_json.get('ctx_area_nk200', '') or '').strip()
if tr_cont in ['M', 'F'] and nk200: time.sleep(0.3); continue
else: break
else: return False
return valid_orders
# π¨ MODIFIED: [Case 18] λ‘컬 μμ½ μ€λ
μ· νκΈ° λ° KIS μμ₯ μ§μ μ°λ
def get_reservation_orders(self, ticker, start_date, end_date):
excg_cd = self._get_exchange_code(ticker, target_api="ORDER")
valid_orders = []
fk200, nk200 = "", ""
for attempt in range(15):
# π¨ MODIFIED: [Thundering Herd μꡬ μκ°] ννΈνλ sleep μκ° (GlobalThrottle μμ)
params = {
"CANO": self.cano,
"ACNT_PRDT_CD": self.acnt_prdt_cd,
"INQR_STRT_DT": start_date,
"INQR_END_DT": end_date,
"INQR_DVSN_CD": "00",
"OVRS_EXCG_CD": excg_cd,
"PRDT_TYPE_CD": "",
"CTX_AREA_FK200": fk200,
"CTX_AREA_NK200": nk200
}
headers = self._get_header("TTTT3039R")
# π¨ MODIFIED: [νμ΄μ§ ν©νΈ μμ ] λ€μ νμ΄μ§ μμ² μ "tr_cont": "N" κ°μ μ£Όμ
if fk200 or nk200: headers["tr_cont"] = "N"
res, resp_json = self._api_request("GET", f"{self.base_url}/uapi/overseas-stock/v1/trading/order-resv-list", headers, params=params)
if res and resp_json.get('rt_cd') == '0':
# π¨ MODIFIED: [Iterable λΆκ΄΄ λ°©μ΄] None μ μ
μ []λ‘ λ¨λ½ νκ°
output = resp_json.get('output') or []
if isinstance(output, dict): output = [output]
if not isinstance(output, list): output = []
valid_orders.extend([item for item in output if isinstance(item, dict) and item.get('pdno') == ticker])
tr_cont = res.headers.get('tr_cont', '') if hasattr(res, 'headers') else ''
fk200 = str(resp_json.get('ctx_area_fk200', '') or '').strip()
nk200 = str(resp_json.get('ctx_area_nk200', '') or '').strip()
if tr_cont in ['M', 'F'] and nk200:
time.sleep(0.3)
continue
else:
break
else:
break
return valid_orders
def cancel_all_orders_safe(self, ticker, side=None):
for i in range(3):
orders = self.get_unfilled_orders_detail(ticker)
if orders is False: return False
if not orders: return True
target_orders = orders
if side == "BUY": target_orders = [o for o in orders if o.get('sll_buy_dvsn_cd') == '02']
elif side == "SELL": target_orders = [o for o in orders if o.get('sll_buy_dvsn_cd') == '01']
if not target_orders: return True
for o in target_orders:
# π¨ MODIFIED: ννΈνλ sleep μκ°
self.cancel_order(ticker, o.get('odno'))
# π¨ MODIFIED: [μ΄λ²€νΈ 루ν κ΅μ°© λ°©μ΄] 5.0μ΄μ κΈ΄ λκΈ° μκ°μ 1.0μ΄λ‘ λ¨μΆνμ¬ μμ μ€μΌμ€λ¬ TimeoutError μμ² λ΄μ
time.sleep(1.0)
final_orders = self.get_unfilled_orders_detail(ticker)
if final_orders is False: return False
# π¨ MODIFIED: [State Mismatch μΉλͺ
μ κ²°ν¨ μμ ] side is None(μ 체 μ·¨μ) μ λλ½λλ λ©±λ±μ± κ²μ¦ λ
Όλ¦¬ μλ²½ 볡ꡬ
failed_orders = [o for o in final_orders if side is None or (side == "BUY" and o.get('sll_buy_dvsn_cd') == '02') or (side == "SELL" and o.get('sll_buy_dvsn_cd') == '01')]
return len(failed_orders) == 0
def cancel_targeted_orders(self, ticker, side, target_ord_dvsn):
sll_buy_cd = '02' if side == "BUY" else '01'
orders = self.get_unfilled_orders_detail(ticker)
if not orders: return 0
target_orders = [o for o in orders if o.get('sll_buy_dvsn_cd') == sll_buy_cd and str(o.get('ord_dvsn_cd') or o.get('ord_dvsn') or '') == target_ord_dvsn]
for o in target_orders:
# π¨ MODIFIED: ννΈνλ sleep μκ°
self.cancel_order(ticker, o.get('odno'))
time.sleep(0.3)
return len(target_orders)
def cancel_orders_by_price(self, ticker, side, target_prices):
sll_buy_cd = '02' if side == "BUY" else '01'
orders = self.get_unfilled_orders_detail(ticker)
if not orders: return 0
target_orders = []
# π¨ MODIFIED: [λ°°μ΄ λ΄λΆ Float μ€μΌ μ°¨λ¨ & κ²°μΈ‘μΉ λ°©μ΄] λ¬Έμμ΄ μ μ
λ°©μ§ μ¬μ λ§΅ν λ° λ¨λ½ νκ°
safe_targets = [self._safe_float(tp) for tp in (target_prices or [])]
for o in orders:
if o.get('sll_buy_dvsn_cd') == sll_buy_cd:
o_price = 0.0
for rp in [o.get('ft_ord_unpr3', 0), o.get('ord_unpr', 0), o.get('ovrs_ord_unpr', 0)]:
# π¨ MODIFIED: [Insight 14] String-Float λ§Ήλ
μ± μ΄λ λν
try:
val = self._safe_float(rp)
if val > 0: o_price = val; break
except (TypeError, ValueError):
pass
for tp in safe_targets:
if o_price > 0 and abs(o_price - tp) < 0.005: target_orders.append(o); break
for o in target_orders:
# π¨ MODIFIED: ννΈνλ sleep μκ°
self.cancel_order(ticker, o.get('odno'))
time.sleep(0.3)
return len(target_orders)
def send_order(self, ticker, side, qty, price, order_type="LIMIT", start_time=None, end_time=None):
# π¨ MODIFIED: [Insight 14] String-Float λ§Ήλ
μ± μ΄λ λν
try: order_qty = int(self._safe_float(qty))
except (TypeError, ValueError): return {'rt_cd': '999', 'msg1': f'μ ν¨νμ§ μμ μ£Όλ¬Έ μλ: {qty!r}'}
if order_qty <= 0: return {'rt_cd': '999', 'msg1': f'μλ μ€λ₯: {qty}'}
for attempt in range(3):
# π¨ MODIFIED: ννΈνλ sleep μκ°
tr_id = "TTTT1002U" if side == "BUY" else "TTTT1006U"
excg_cd = self._get_exchange_code(ticker, target_api="ORDER")
ord_dvsn = {"LOC": "34", "MOC": "33", "LOO": "32", "MOO": "31", "VWAP": "36"}.get(order_type, "00")
final_price = 0 if order_type in ["MOC", "MOO"] else self._ceil_2(price)
if order_type not in ["MOC", "MOO"] and final_price <= 0.0: return {'rt_cd': '999', 'msg1': f'κ°κ²© μ€λ₯: {price}'}
sll_type = "00" if side == "SELL" else ""
body = {
"CANO": self.cano, "ACNT_PRDT_CD": self.acnt_prdt_cd, "OVRS_EXCG_CD": excg_cd,
"PDNO": ticker, "ORD_QTY": str(order_qty), "OVRS_ORD_UNPR": str(final_price),
"ORD_SVR_DVSN_CD": "0", "ORD_DVSN": ord_dvsn, "SLL_TYPE": sll_type
}
if order_type == "VWAP":
if start_time and end_time:
body["ALGO_ORD_TMD_DVSN_CD"] = "00"
body["START_TIME"] = start_time
body["END_TIME"] = end_time
else:
body["ALGO_ORD_TMD_DVSN_CD"] = "02"
res = self._call_api(tr_id, "/uapi/overseas-stock/v1/trading/order", "POST", body=body)
# π¨ MODIFIED: [TypeError λΆκ΄΄ λ°©μ΄] msg1μ΄ NoneμΌ κ²½μ° any() 루νμμ λ°μνλ μμΈ μμ² λ΄μ
safe_msg = str(res.get('msg1') or '')
if res.get('rt_cd') != '0':
if attempt < 2 and any(x in safe_msg for x in ["κ±°λμ", "μμ₯", "exchange", "μ½λ"]):
if ticker in self._excg_cd_cache: del self._excg_cd_cache[ticker]
time.sleep(1.0 * (2 ** attempt))
continue
# π¨ MODIFIED: [Case 36 μ λ λ°©μ΄λ§ ν©νΈ κ΅μ ] MOC λ€ν μ λ¨μ -5% νν₯ λ‘μ§μ μκ°νκ³ , λ§€μ 1νΈκ°(Bid Price) ν©νΈ ν격μΌλ‘ κ΅μ
if order_type == "MOC":
logging.warning(f"π¨ [Case 36 λ°©μ΄λ§] KIS MOC μ£Όλ¬Έ 리μ κ°μ§ ({safe_msg}). λ§€μ 1νΈκ° λ€ν ν΄λ°± κ°λ!")
bid_p = self.get_bid_price(ticker)
if bid_p > 0:
dump_price = bid_p
logging.info(f"π [{ticker}] MOC β LIMIT(${dump_price:.2f}, λ§€μ 1νΈκ°) μ ν μ격 μ μ‘")
# π¨ [무ν 루ν μ°¨λ¨] LIMITμΌλ‘ μ¬κ· νΈμΆνλ―λ‘ 2μ°¨ 리μ μ ν΄λ°±μ΄ μ€λ³΅ 격λ°λμ§ μμ
return self.send_order(ticker, side, qty, dump_price, order_type="LIMIT")
else:
curr_p = self.get_current_price(ticker)
if curr_p > 0:
dump_price = self._ceil_2(curr_p * 0.95)
logging.info(f"π [{ticker}] MOC β LIMIT(${dump_price:.2f}, ν΅μ μ§μ° -5% ν΄λ°±) μ ν μ격 μ μ‘")
return self.send_order(ticker, side, qty, dump_price, order_type="LIMIT")
return {'rt_cd': str(res.get('rt_cd') or '999'), 'msg1': safe_msg or 'μ€λ₯', 'odno': ''}
# π¨ MODIFIED: [AttributeError λΆκ΄΄ λ°©μ΄] output κ°μ²΄ μΆμΆ μ μμ λ¨λ½ νκ°
out = res.get('output') or {}
if not isinstance(out, dict): out = {}
return {'rt_cd': str(res.get('rt_cd') or '999'), 'msg1': safe_msg or 'μ€λ₯', 'odno': str(out.get('ODNO') or '')}
return {'rt_cd': '999', 'msg1': 'κ±°λμ μΊμ μ¬μλ μ΄κ³Ό'}
def cancel_order(self, ticker, order_id):
# π¨ MODIFIED: ννΈνλ sleep μκ°
excg_cd = self._get_exchange_code(ticker, target_api="ORDER")
body = {"CANO": self.cano, "ACNT_PRDT_CD": self.acnt_prdt_cd, "OVRS_EXCG_CD": excg_cd, "PDNO": ticker, "ORGN_ODNO": order_id, "RVSE_CNCL_DVSN_CD": "02", "ORD_QTY": "0", "OVRS_ORD_UNPR": "0", "ORD_SVR_DVSN_CD": "0"}
# π¨ MODIFIED: [Case 30 ν©νΈ κ΅μ ] μ·¨μ μ£Όλ¬Έ API μλ΅ κ°μ²΄ λ°ν λ°°μ κ°μ μ΄μ
return self._call_api("TTTT1004U", "/uapi/overseas-stock/v1/trading/order-rvsecncl", "POST", body=body)
def send_daytime_order(self, ticker, side, qty, price):
# π¨ MODIFIED: ννΈνλ sleep μκ°
# π¨ MODIFIED: [μ΅μ’
λ¬΄κ²°μ± μμ ] μλ μ£Όλ¬Έ μ Float μλμ΄ μ£Όμ
λμ΄ KIS μλ²μμ 리μ λλ νμμ λ§κΈ° μν΄ int κ°μ νλ³ν μ΄λ μ£Όμ
try: order_qty = int(self._safe_float(qty))
except: return {'rt_cd': '999', 'msg1': 'μλ μ€λ₯'}
if order_qty <= 0: return {'rt_cd': '999', 'msg1': 'μλ μ€λ₯'}
tr_id = "TTTS6036U" if side == "BUY" else "TTTS6037U"
excg_cd = self._get_exchange_code(ticker, target_api="ORDER")
body = {"CANO": self.cano, "ACNT_PRDT_CD": self.acnt_prdt_cd, "OVRS_EXCG_CD": excg_cd, "PDNO": ticker, "ORD_QTY": str(order_qty), "OVRS_ORD_UNPR": str(self._ceil_2(price)), "CTAC_TLNO": "", "MGCO_APTM_ODNO": "", "ORD_SVR_DVSN_CD": "0", "ORD_DVSN": "00"}
res = self._call_api(tr_id, "/uapi/overseas-stock/v1/trading/daytime-order", "POST", body=body)
# π¨ MODIFIED: [AttributeError λΆκ΄΄ λ°©μ΄] output κ°μ²΄ μΆμΆ μ μμ λ¨λ½ νκ°
out = res.get('output') or {}
if not isinstance(out, dict): out = {}
safe_msg = str(res.get('msg1') or 'μ€λ₯')
return {'rt_cd': str(res.get('rt_cd') or '999'), 'msg1': safe_msg, 'odno': str(out.get('ODNO') or '')}
def cancel_daytime_order(self, ticker, order_id, qty="100", price="0"):
# π¨ MODIFIED: ννΈνλ sleep μκ°
excg_cd = self._get_exchange_code(ticker, target_api="ORDER")
# π¨ MODIFIED: [μ΅μ’
λ¬΄κ²°μ± μμ ] Float λ¬Έμμ΄ λ°©μ΄
safe_qty = str(int(self._safe_float(qty)))
safe_price = str(self._safe_float(price))
body = {"CANO": self.cano, "ACNT_PRDT_CD": self.acnt_prdt_cd, "OVRS_EXCG_CD": excg_cd, "PDNO": ticker, "ORGN_ODNO": order_id, "RVSE_CNCL_DVSN_CD": "02", "ORD_QTY": safe_qty, "OVRS_ORD_UNPR": safe_price, "CTAC_TLNO": "", "MGCO_APTM_ODNO": "", "ORD_SVR_DVSN_CD": "0"}
return self._call_api("TTTS6038U", "/uapi/overseas-stock/v1/trading/daytime-order-rvsecncl", "POST", body=body)
def send_reservation_order(self, ticker, side, qty, price, order_type="LIMIT"):
# π¨ MODIFIED: ννΈνλ sleep μκ°
# π¨ MODIFIED: [Insight 14] String-Float λ§Ήλ
μ± μ΄λ λν
try: order_qty = int(self._safe_float(qty))
except: return {'rt_cd': '999', 'msg1': 'μλ μ€λ₯'}
tr_id = "TTTT3014U" if side == "BUY" else "TTTT3016U"
excg_cd = self._get_exchange_code(ticker, target_api="ORDER")
final_price = str(self._ceil_2(price))
body = {
"CANO": self.cano, "ACNT_PRDT_CD": self.acnt_prdt_cd, "PDNO": ticker,
"OVRS_EXCG_CD": excg_cd, "FT_ORD_QTY": str(order_qty)
}
if order_type == "LOC":
body["ORD_DVSN"] = "34"
body["FT_ORD_UNPR3"] = final_price
else:
body["ORD_DVSN"] = "00"
body["FT_ORD_UNPR3"] = final_price
res = self._call_api(tr_id, "/uapi/overseas-stock/v1/trading/order-resv", "POST", body=body)
rt_cd = str(res.get('rt_cd') or '999')
msg1 = str(res.get('msg1') or 'μ€λ₯')
# π¨ MODIFIED: [AttributeError λΆκ΄΄ λ°©μ΄] output κ°μ²΄ μΆμΆ μ μμ λ¨λ½ νκ°
out = res.get('output') or {}
if not isinstance(out, dict): out = {}
odno = str(out.get('ODNO') or '')
return {'rt_cd': rt_cd, 'msg1': msg1, 'odno': odno}
def cancel_reservation_order(self, order_date, order_id):
# π¨ MODIFIED: ννΈνλ sleep μκ°
body = {"CANO": self.cano, "ACNT_PRDT_CD": self.acnt_prdt_cd, "RSVN_ORD_RCIT_DT": order_date, "OVRS_RSVN_ODNO": order_id}
return self._call_api("TTTT3017U", "/uapi/overseas-stock/v1/trading/order-resv-ccnl", "POST", body=body)
def get_execution_history(self, ticker, start_date, end_date):
excg_cd = self._get_exchange_code(ticker, target_api="ORDER")
odno_map = {}
# π¨ MODIFIED: [νμ΄μ§ ν©νΈ μμ ] KIS μμ₯ 100건 μ΄κ³Ό μ λ€μ νμ΄μ§ ν ν° λ³μλ₯Ό 루ν μΈλΆλ‘ μ μ§ λ°°μΉ
fk200, nk200 = "", ""
for attempt in range(10):
# π¨ MODIFIED: ννΈνλ sleep μκ°
# π¨ MODIFIED: κ°±μ λ μ°μ μ‘°ν ν ν°(fk200, nk200)μ paramsμ μ λ° μ£Όμ
νμ¬ μ λ Ή 루ν λΆκ΄΄ μ°¨λ¨
params = {"CANO": self.cano, "ACNT_PRDT_CD": self.acnt_prdt_cd, "PDNO": ticker, "ORD_STRT_DT": start_date, "ORD_END_DT": end_date, "SLL_BUY_DVSN": "00", "CCLD_NCCS_DVSN": "00", "OVRS_EXCG_CD": excg_cd, "SORT_SQN": "DS", "CTX_AREA_FK200": fk200, "CTX_AREA_NK200": nk200}
headers = self._get_header("TTTS3035R")
# π¨ MODIFIED: [νμ΄μ§ ν©νΈ μμ ] λ€μ νμ΄μ§ μμ² μ "tr_cont": "N" κ°μ μ£Όμ
if fk200 or nk200: headers["tr_cont"] = "N"
res, resp_json = self._api_request("GET", f"{self.base_url}/uapi/overseas-stock/v1/trading/inquire-ccnl", headers, params=params)
if res and resp_json.get('rt_cd') == '0':
# π¨ MODIFIED: [Iterable λΆκ΄΄ λ°©μ΄] None μ μ
μ []λ‘ λ¨λ½ νκ°
output = resp_json.get('output') or []
if isinstance(output, dict): output = [output]
if not isinstance(output, list): output = []
for item in output:
if not isinstance(item, dict): continue
try:
# π¨ MODIFIED: [Insight 14] String-Float λ§Ήλ
μ± μ΄λ λν
iq, ip = self._safe_float(item.get('ft_ccld_qty', 0)), self._safe_float(item.get('ft_ccld_unpr3', 0))
odno = item.get('odno', f"__nk_{id(item)}")
if iq > 0:
if odno not in odno_map:
odno_map[odno] = {"item": dict(item), "total_qty": iq, "total_amt": iq * ip}
else:
odno_map[odno]["total_qty"] += iq
odno_map[odno]["total_amt"] += (iq * ip)
except: continue
# π¨ MODIFIED: [νμ΄μ§ ν©νΈ μμ ] λ€μ νμ΄μ§ νΈμΆμ μν μ°μ μ‘°ν ν ν° νμ±
fk200 = str(resp_json.get('ctx_area_fk200', '') or '').strip()
nk200 = str(resp_json.get('ctx_area_nk200', '') or '').strip()
# π¨ MODIFIED: [AttributeError λΆκ΄΄ λ°©μ΄] res.headers μμ μ€μ½ν νμΈ λ° κ°±μ λ nk200μ΄ μμ λλ§ continue
if hasattr(res, 'headers') and res.headers.get('tr_cont', '') in ['M', 'F'] and nk200:
time.sleep(0.3)
continue
else: break
else: break
# π¨ MODIFIED: [μΉλͺ
μ λ
Όλ¦¬ κ²°ν¨ μμ ] λμ
λ리 μΈν¨νΉ μ€λ²λΌμ΄λ λ§Ήμ κ΅μ (μλ³Έ λ°μ΄ν° μ°μ λ°°μΉ)
return [{**d["item"], "ft_ccld_qty": str(d["total_qty"]), "ft_ccld_unpr3": str(d["total_amt"]/d["total_qty"] if d["total_qty"]>0 else 0)} for d in odno_map.values()]
def get_genesis_ledger(self, ticker, limit_date_str=None):
_, h = self.get_account_balance()
if not h: return None, 0, 0.0
t_info = h.get(ticker, {'qty': 0, 'avg': 0.0})
curr_qty = int(self._safe_float(t_info.get('qty', 0)))
if curr_qty == 0: return [], 0, 0.0
ledger_records, est, target_date, loop = [], ZoneInfo('America/New_York'), datetime.datetime.now(ZoneInfo('America/New_York')), 0
while curr_qty > 0 and loop < 365:
# π¨ MODIFIED: ννΈνλ sleep μκ° (get_execution_historyμμ API νΈμΆ μ μλ μ§μ°λ¨)
if target_date.weekday() < 5: loop += 1
date_str = target_date.strftime('%Y%m%d')
if limit_date_str and date_str < limit_date_str: break
execs = self.get_execution_history(ticker, date_str, date_str)
if execs:
# π¨ MODIFIED: [Sort λ°νμ λΆκ΄΄ λ°©μ΄] ord_tmd κ²°μΈ‘μΉ(None) μ μ
μ str() μΊμ€ν
λ° '000000' ν΄λ°±μΌλ‘ TypeError μλ°©
execs.sort(key=lambda x: str(x.get('ord_tmd') or '000000'), reverse=True)
for ex in execs:
# π¨ MODIFIED: [Insight 14] String-Float λ§Ήλ
μ± μ΄λ λν
side, eq, ep = str(ex.get('sll_buy_dvsn_cd') or ''), int(self._safe_float(ex.get('ft_ccld_qty', 0))), self._safe_float(ex.get('ft_ccld_unpr3', 0))
rq = eq
if side == "02":
if curr_qty <= eq: rq, curr_qty = curr_qty, 0
else: curr_qty -= eq
else: curr_qty += eq
ledger_records.append({'date': f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:]}", 'side': "BUY" if side=="02" else "SELL", 'qty': rq, 'price': ep})
if curr_qty == 0: break
target_date -= datetime.timedelta(days=1);
time.sleep(0.1)
if curr_qty > 0: ledger_records.append({'date': 'INCOMPLETE', 'side': 'UNKNOWN', 'qty': curr_qty, 'price': self._safe_float(t_info.get('avg', 0.0)), 'is_incomplete': True})
ledger_records.reverse()
return ledger_records, int(self._safe_float(t_info.get('qty', 0))), self._safe_float(t_info.get('avg', 0.0))