pyqqq 0.12.168__tar.gz → 0.12.170__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.
Potentially problematic release.
This version of pyqqq might be problematic. Click here for more details.
- {pyqqq-0.12.168 → pyqqq-0.12.170}/PKG-INFO +1 -1
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyproject.toml +1 -1
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/broker.py +153 -38
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/environment.py +8 -3
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/domestic_stock.py +11 -1
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/simple.py +17 -7
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/daily.py +3 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/minutes.py +2 -2
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/realtime.py +1 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/market_schedule.py +1 -1
- {pyqqq-0.12.168 → pyqqq-0.12.170}/README.md +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/__init__.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/ai/__init__.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/ai/daily.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/ai/domestic.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/ai/market_schedule.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/ai/minute.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/__init__.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/logger.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/positionprovider.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/strategy.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/utils.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/wallclock.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/__init__.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/ebest/__init__.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/ebest/domestic_stock.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/ebest/oauth.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/ebest/simple.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/ebest/tr_client.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/helper.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/__init__.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/oauth.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/overseas_stock.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/simple_overseas.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/tr_client.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/multiprocess_tracker.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/tracker.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/config.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/__init__.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/domestic.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/index.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/overseas.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/ticks.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/us_stocks.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/datatypes.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/executors/__init__.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/executors/hook.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/__init__.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/api_client.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/array.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/casting.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/compute.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/copycat.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/daily_tickers.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/display.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/kvstore.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/limiter.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/local_cache.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/logger.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/mock_api.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/position_classifier.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/retry.py +0 -0
- {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/singleton.py +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import datetime as dtm
|
|
2
2
|
import os
|
|
3
|
+
import fcntl
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
4
5
|
from dataclasses import asdict
|
|
5
6
|
from decimal import Decimal
|
|
@@ -243,6 +244,7 @@ class TradingBroker(BaseBroker):
|
|
|
243
244
|
data_api: Union[KISSimpleDomesticStock, KISSimpleOverseasStock],
|
|
244
245
|
trading_api: Union[KISSimpleDomesticStock, KISSimpleOverseasStock],
|
|
245
246
|
clock: WallClock,
|
|
247
|
+
market_nxt_on: bool = False,
|
|
246
248
|
):
|
|
247
249
|
"""TradingBroker 클래스의 초기화 메서드입니다.
|
|
248
250
|
|
|
@@ -259,25 +261,62 @@ class TradingBroker(BaseBroker):
|
|
|
259
261
|
self.logger = get_logger("TradingBroker", clock)
|
|
260
262
|
self.data_api = data_api
|
|
261
263
|
self.trading_api = trading_api
|
|
264
|
+
self.market_nxt_on = market_nxt_on
|
|
262
265
|
|
|
263
266
|
def get_account(self) -> dict:
|
|
264
267
|
return self.trading_api.get_account()
|
|
265
268
|
|
|
266
|
-
def get_price(self, code: str) -> Decimal:
|
|
267
|
-
|
|
269
|
+
def get_price(self, code: str, data_exchange: Optional[DataExchange] = None) -> Decimal:
|
|
270
|
+
if data_exchange:
|
|
271
|
+
price_data = self.data_api.get_price(code, data_exchange=data_exchange)
|
|
272
|
+
else:
|
|
273
|
+
if self.market_nxt_on:
|
|
274
|
+
price_data = self.data_api.get_price(code, data_exchange=DataExchange.NXT)
|
|
275
|
+
if price_data.get("current_price") == 0:
|
|
276
|
+
price_data = self.data_api.get_price(code, data_exchange=DataExchange.KRX)
|
|
277
|
+
else:
|
|
278
|
+
price_data = self.data_api.get_price(code, data_exchange=DataExchange.KRX)
|
|
279
|
+
|
|
268
280
|
result = price_data.get("current_price")
|
|
269
281
|
if result is None:
|
|
270
282
|
raise ValueError(f"Current price not found: {code}")
|
|
271
283
|
return Decimal(str(result))
|
|
272
284
|
|
|
273
|
-
def get_minute_price(self, code) -> pd.DataFrame:
|
|
274
|
-
|
|
285
|
+
def get_minute_price(self, code: str, data_exchange: Optional[DataExchange] = None) -> pd.DataFrame:
|
|
286
|
+
if data_exchange:
|
|
287
|
+
result = self.data_api.get_today_minute_data(code, data_exchange=data_exchange)
|
|
288
|
+
else:
|
|
289
|
+
if self.market_nxt_on:
|
|
290
|
+
result = self.data_api.get_today_minute_data(code, data_exchange=DataExchange.NXT)
|
|
291
|
+
if result.iloc[-1].close == 0:
|
|
292
|
+
result = self.data_api.get_today_minute_data(code, data_exchange=DataExchange.KRX)
|
|
293
|
+
else:
|
|
294
|
+
result = self.data_api.get_today_minute_data(code, data_exchange=DataExchange.KRX)
|
|
295
|
+
return result
|
|
275
296
|
|
|
276
|
-
def get_daily_price(self, code: str, from_date: dtm.date, to_date: dtm.date) -> pd.DataFrame:
|
|
277
|
-
|
|
297
|
+
def get_daily_price(self, code: str, from_date: dtm.date, to_date: dtm.date, data_exchange: Optional[DataExchange] = None) -> pd.DataFrame:
|
|
298
|
+
if data_exchange:
|
|
299
|
+
result = self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True, data_exchange=data_exchange)
|
|
300
|
+
else:
|
|
301
|
+
if self.market_nxt_on:
|
|
302
|
+
result = self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True, data_exchange=DataExchange.NXT)
|
|
303
|
+
if result.empty:
|
|
304
|
+
result = self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True, data_exchange=DataExchange.KRX)
|
|
305
|
+
else:
|
|
306
|
+
result = self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True, data_exchange=DataExchange.KRX)
|
|
307
|
+
return result
|
|
278
308
|
|
|
279
|
-
def get_orderbook(self):
|
|
280
|
-
|
|
309
|
+
def get_orderbook(self, code: str, data_exchange: Optional[DataExchange] = None):
|
|
310
|
+
if data_exchange:
|
|
311
|
+
result = self.data_api.get_orderbook(code, data_exchange=data_exchange)
|
|
312
|
+
else:
|
|
313
|
+
if self.market_nxt_on:
|
|
314
|
+
result = self.data_api.get_orderbook(code, data_exchange=DataExchange.NXT)
|
|
315
|
+
if not result:
|
|
316
|
+
result = self.data_api.get_orderbook(code, data_exchange=DataExchange.KRX)
|
|
317
|
+
else:
|
|
318
|
+
result = self.data_api.get_orderbook(code, data_exchange=DataExchange.KRX)
|
|
319
|
+
return result
|
|
281
320
|
|
|
282
321
|
def get_pending_orders(self):
|
|
283
322
|
return self.trading_api.get_pending_orders()
|
|
@@ -383,6 +422,7 @@ class MockBroker(BaseBroker):
|
|
|
383
422
|
time_unit="minutes",
|
|
384
423
|
current_data_handling: Literal["include", "virtual", "exclude"] = "virtual",
|
|
385
424
|
local_cache_path: str = LOCAL_CACHE_PATH,
|
|
425
|
+
market_nxt_on: bool = False,
|
|
386
426
|
):
|
|
387
427
|
"""MockBroker 클래스의 초기화 메서드입니다.
|
|
388
428
|
|
|
@@ -398,11 +438,14 @@ class MockBroker(BaseBroker):
|
|
|
398
438
|
- "virtual": 직전(1분전)까지의 데이터 + 현재(분)의 시가로 통일한 가상 데이터 반환 (기본값)
|
|
399
439
|
- "exclude": 직전(1분전)까지의 데이터만 반환
|
|
400
440
|
local_cache_path (str, optional): 데이터 로컬 캐시 경로. Defaults to "./data".
|
|
441
|
+
market_nxt_on (bool, optional): 넥스트레이드 시장 활성화 여부. Defaults to False.
|
|
401
442
|
"""
|
|
402
443
|
self.logger = get_logger("MockBroker", clock)
|
|
403
444
|
self.clock = clock
|
|
404
445
|
self.clock.on_time_change = self.on_time_change
|
|
405
446
|
self.market = market
|
|
447
|
+
self.market_nxt_on = market_nxt_on
|
|
448
|
+
self.logger.info(f"init market_nxt_on={self.market_nxt_on}")
|
|
406
449
|
|
|
407
450
|
self.next_order_no = 1000
|
|
408
451
|
self.cash = 1_000_000_000
|
|
@@ -470,8 +513,12 @@ class MockBroker(BaseBroker):
|
|
|
470
513
|
def typed_result(value) -> Decimal:
|
|
471
514
|
return Decimal(value).quantize(Decimal("0.0000")) if self.market == "us_stock" else Decimal(int(value))
|
|
472
515
|
|
|
473
|
-
|
|
474
|
-
|
|
516
|
+
market_schedule = get_market_schedule(today, exchange=self._get_exchange_code())
|
|
517
|
+
open_time = market_schedule.open_time
|
|
518
|
+
if self.market_nxt_on:
|
|
519
|
+
open_time = dtm.time(8, 0, 0)
|
|
520
|
+
|
|
521
|
+
if now.time() < open_time:
|
|
475
522
|
last_trading_day = get_last_trading_day(today, exchange=self._get_exchange_code())
|
|
476
523
|
df = self.get_daily_price(code, last_trading_day, last_trading_day) # 전일 종가는 일봉, 분봉 구분이 필요 없다.
|
|
477
524
|
return typed_result(df["close"].iloc[-1])
|
|
@@ -496,7 +543,8 @@ class MockBroker(BaseBroker):
|
|
|
496
543
|
|
|
497
544
|
def get_minute_price(self, code: str) -> pd.DataFrame:
|
|
498
545
|
"""
|
|
499
|
-
self.clock 기준
|
|
546
|
+
self.clock 기준 당일의 정규장 시작부터 현재 시각 (이전) 까지의 분봉 데이터를 조회합니다.
|
|
547
|
+
단, market_nxt_on 이면, NXT 가능 종목의 경우 프리마켓, 정규마켓, 애프터마켓의 분봉을 모두 조회합니다.
|
|
500
548
|
|
|
501
549
|
Args:
|
|
502
550
|
code (str): 종목 코드
|
|
@@ -509,13 +557,21 @@ class MockBroker(BaseBroker):
|
|
|
509
557
|
if df.empty:
|
|
510
558
|
return df
|
|
511
559
|
|
|
512
|
-
# 정규장 정보로만 거르기
|
|
560
|
+
# 정규장 정보로만 거르기 ==================================
|
|
513
561
|
market_schedule = get_market_schedule(today, "NYSE" if self.market == "us_stock" else "KRX")
|
|
514
562
|
open_time = dtm.datetime.combine(today, market_schedule.open_time)
|
|
515
563
|
close_time = dtm.datetime.combine(today, market_schedule.close_time)
|
|
564
|
+
|
|
565
|
+
# TODO: us_stock 에 대해서는 따로 적용해야 함.
|
|
566
|
+
is_nxt = df.index[0] == dtm.datetime.combine(today, dtm.time(8, 0, 0))
|
|
567
|
+
if self.market_nxt_on and is_nxt:
|
|
568
|
+
open_time = dtm.datetime.combine(today, dtm.time(8, 0, 0))
|
|
569
|
+
close_time = dtm.datetime.combine(today, dtm.time(20, 0, 0))
|
|
570
|
+
|
|
516
571
|
if df.index.tz is not None:
|
|
517
572
|
df.index = df.index.tz_localize(None)
|
|
518
573
|
df = df[(df.index >= open_time) & (df.index <= close_time)]
|
|
574
|
+
# ========================================================
|
|
519
575
|
|
|
520
576
|
# 미래 정보 없애기
|
|
521
577
|
now = pd.Timestamp(self.clock.now())
|
|
@@ -558,34 +614,71 @@ class MockBroker(BaseBroker):
|
|
|
558
614
|
|
|
559
615
|
@lru_cache(maxsize=40)
|
|
560
616
|
def _get_minute_price_with_cache(self, date, code) -> pd.DataFrame:
|
|
561
|
-
|
|
617
|
+
"""
|
|
618
|
+
market_nxt_on 이면, NXT 프리마켓, 정규마켓, 애프터마켓의 분봉이 모두 반환된다.
|
|
619
|
+
"""
|
|
620
|
+
folder_name = "nxt_minutes" if self.market_nxt_on else "minutes"
|
|
621
|
+
file_path = f"{self.local_cache_path}/{folder_name}/{date}/{code}.csv" if self.local_cache_path else None
|
|
562
622
|
if file_path and os.path.exists(file_path):
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
623
|
+
try:
|
|
624
|
+
with open(file_path, 'r') as f:
|
|
625
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
626
|
+
try:
|
|
627
|
+
df = pd.read_csv(f)
|
|
628
|
+
df['time'] = pd.to_datetime(df['time'])
|
|
629
|
+
df.set_index('time', inplace=True)
|
|
630
|
+
finally:
|
|
631
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
632
|
+
except Exception as e:
|
|
633
|
+
self.logger.exception(e)
|
|
634
|
+
if os.path.exists(file_path):
|
|
635
|
+
os.remove(file_path)
|
|
636
|
+
df = self._fetch_minute_price(date, code)
|
|
637
|
+
else:
|
|
638
|
+
df = self._fetch_minute_price(date, code)
|
|
567
639
|
|
|
640
|
+
return df
|
|
641
|
+
|
|
642
|
+
def _fetch_minute_price(self, date, code) -> pd.DataFrame:
|
|
643
|
+
folder_name = "nxt_minutes" if self.market_nxt_on else "minutes"
|
|
644
|
+
file_path = f"{self.local_cache_path}/{folder_name}/{date}/{code}.csv" if self.local_cache_path else None
|
|
645
|
+
|
|
646
|
+
if self.market == "us_stock":
|
|
647
|
+
dfs = get_us_minute_data(date, [code])
|
|
568
648
|
else:
|
|
569
|
-
if self.
|
|
570
|
-
dfs =
|
|
649
|
+
if self.market_nxt_on:
|
|
650
|
+
dfs = get_kr_minute_data(date, [code], dtm.timedelta(minutes=1), source="kis", adjusted=True, exchange=DataExchange.NXT)
|
|
651
|
+
if dfs[code].empty: # NXT 에 해당 종목이 없는 경우 KRX 의 결과값을 반환한다. 하지만 로컬 캐시 폴더는 NXT 기준으로 저장한다.
|
|
652
|
+
dfs = get_kr_minute_data(date, [code], dtm.timedelta(minutes=1), source="kis", adjusted=True, exchange=DataExchange.KRX)
|
|
571
653
|
else:
|
|
572
|
-
dfs = get_kr_minute_data(date, [code], dtm.timedelta(minutes=1), source="kis", adjusted=True)
|
|
654
|
+
dfs = get_kr_minute_data(date, [code], dtm.timedelta(minutes=1), source="kis", adjusted=True, exchange=DataExchange.KRX)
|
|
573
655
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
656
|
+
df = dfs[code]
|
|
657
|
+
if df.empty:
|
|
658
|
+
return df
|
|
577
659
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
660
|
+
if self.local_cache_path:
|
|
661
|
+
if not os.path.exists(f"{self.local_cache_path}/{folder_name}/{date}"):
|
|
662
|
+
os.makedirs(f"{self.local_cache_path}/{folder_name}/{date}")
|
|
581
663
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
664
|
+
df.reset_index(inplace=True)
|
|
665
|
+
with open(file_path, 'w') as f:
|
|
666
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
667
|
+
try:
|
|
668
|
+
df.to_csv(f, index=False)
|
|
669
|
+
except Exception as e:
|
|
670
|
+
self.logger.exception(e)
|
|
671
|
+
finally:
|
|
672
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
673
|
+
df.set_index("time", inplace=True)
|
|
585
674
|
|
|
586
|
-
|
|
675
|
+
return df
|
|
587
676
|
|
|
588
677
|
def get_daily_price(self, code: str, from_date: Optional[dtm.date] = None, end_date: Optional[dtm.date] = None):
|
|
678
|
+
"""
|
|
679
|
+
지정된 기간 동안의 정규장 시작부터 현재 시각 (이전) 까지의 일봉 데이터를 조회합니다.
|
|
680
|
+
단, market_nxt_on 이면, NXT 가능 종목의 경우 프리마켓, 정규마켓, 애프터마켓의 일봉을 모두 조회합니다.
|
|
681
|
+
"""
|
|
589
682
|
today = self.clock.today()
|
|
590
683
|
|
|
591
684
|
def _exclude_today(df: pd.DataFrame) -> pd.DataFrame:
|
|
@@ -604,7 +697,12 @@ class MockBroker(BaseBroker):
|
|
|
604
697
|
if self.market == "us_stock":
|
|
605
698
|
dfs = get_us_daily_data([code], from_date, end_date, adjusted=True, ascending=True)
|
|
606
699
|
else:
|
|
607
|
-
|
|
700
|
+
if self.market_nxt_on:
|
|
701
|
+
dfs = get_kr_daily_data([code], from_date, end_date, adjusted=True, ascending=True, exchange=DataExchange.NXT)
|
|
702
|
+
if code not in dfs or dfs[code].empty:
|
|
703
|
+
dfs = get_kr_daily_data([code], from_date, end_date, adjusted=True, ascending=True, exchange=DataExchange.KRX)
|
|
704
|
+
else:
|
|
705
|
+
dfs = get_kr_daily_data([code], from_date, end_date, adjusted=True, ascending=True, exchange=DataExchange.KRX)
|
|
608
706
|
|
|
609
707
|
df = dfs[code]
|
|
610
708
|
|
|
@@ -612,16 +710,22 @@ class MockBroker(BaseBroker):
|
|
|
612
710
|
# 마지막 데이터가 오늘인 경우 장중 여부와 설정에따라 구분되어 처리될 필요가 있다.
|
|
613
711
|
now = self.clock.now()
|
|
614
712
|
schedule = get_market_schedule(today, exchange=self._get_exchange_code())
|
|
713
|
+
open_time = schedule.open_time
|
|
714
|
+
close_time = schedule.close_time
|
|
715
|
+
if self.market_nxt_on:
|
|
716
|
+
open_time = dtm.time(8, 0, 0)
|
|
717
|
+
close_time = dtm.time(20, 0, 0)
|
|
718
|
+
|
|
615
719
|
if schedule.full_day_closed:
|
|
616
720
|
pass # end_date 가 폐장일이면 해당 날짜의 데이터는 이미 없음
|
|
617
721
|
else:
|
|
618
722
|
if self.current_data_handling == "include":
|
|
619
723
|
pass
|
|
620
724
|
elif self.current_data_handling == "virtual":
|
|
621
|
-
if now.time() <
|
|
725
|
+
if now.time() < open_time:
|
|
622
726
|
# 장 오픈 전은 전일 데이터 까지만 반환
|
|
623
727
|
df = _exclude_today(df)
|
|
624
|
-
elif now.time() <=
|
|
728
|
+
elif now.time() <= close_time:
|
|
625
729
|
# 장중에는 분봉을 적절히 활용
|
|
626
730
|
minute_df = self.get_minute_price(code)
|
|
627
731
|
if minute_df.empty:
|
|
@@ -665,9 +769,8 @@ class MockBroker(BaseBroker):
|
|
|
665
769
|
return positions
|
|
666
770
|
|
|
667
771
|
def create_order(self, asset_code: str, side: OrderSide, quantity: int, order_type: OrderType = OrderType.MARKET, price: int = 0, exchange: OrderExchange = OrderExchange.KRX) -> str:
|
|
668
|
-
# TODO: exchange에 따라 다른 봉을 보는 처리가 추가로 되어야 함
|
|
669
772
|
price = self.get_price(asset_code) if order_type == OrderType.MARKET else price
|
|
670
|
-
self.logger.info(f"CREATE ORDER: {self._get_asset_name(asset_code)} side:{side} price:{price} quantity:{quantity} order_type:{order_type}")
|
|
773
|
+
self.logger.info(f"CREATE ORDER: {self._get_asset_name(asset_code)} side:{side} price:{price} quantity:{quantity} order_type:{order_type} exchange:{exchange.value}")
|
|
671
774
|
order_no = str(self.next_order_no)
|
|
672
775
|
self.next_order_no += 1
|
|
673
776
|
|
|
@@ -775,14 +878,27 @@ class MockBroker(BaseBroker):
|
|
|
775
878
|
close_time = dtm.datetime.combine(today, market_schedule.close_time)
|
|
776
879
|
preclose_auction_start_time = close_time - dtm.timedelta(minutes=10)
|
|
777
880
|
|
|
881
|
+
nxt_pre_open_time = dtm.datetime.combine(today, dtm.time(8, 0, 0))
|
|
882
|
+
nxt_pre_close_time = dtm.datetime.combine(today, dtm.time(8, 50, 0))
|
|
883
|
+
# nxt_main_open_time = dtm.datetime.combine(today, dtm.time(9, 0, 30))
|
|
884
|
+
# nxt_main_close_time = dtm.datetime.combine(today, dtm.time(15, 20, 0))
|
|
885
|
+
nxt_after_open_time = dtm.datetime.combine(today, dtm.time(15, 40, 0))
|
|
886
|
+
nxt_after_close_time = dtm.datetime.combine(today, dtm.time(20, 0, 0))
|
|
887
|
+
|
|
778
888
|
just_closed = current_time.replace(second=0, microsecond=0) == close_time.replace(second=0, microsecond=0)
|
|
779
889
|
if preclose_auction_start_time - dtm.timedelta(minutes=1) <= current_time <= preclose_auction_start_time + dtm.timedelta(minutes=1):
|
|
780
890
|
# 로그 출력 수 조정
|
|
781
891
|
self.logger.debug(f"just_closed: {just_closed} {self.pending_orders}")
|
|
782
892
|
|
|
783
893
|
if market_schedule.full_day_closed or current_time < open_time or (current_time > close_time and not just_closed):
|
|
784
|
-
self.
|
|
785
|
-
|
|
894
|
+
if self.market_nxt_on and not market_schedule.full_day_closed:
|
|
895
|
+
# market_nxt_on 인 경우 메인 마켓은 KRX 그대로 사용하고 프리마켓과 애프터마켓만 추가한다.
|
|
896
|
+
if current_time < nxt_pre_open_time or nxt_pre_close_time <= current_time < open_time or close_time < current_time < nxt_after_open_time or nxt_after_close_time <= current_time:
|
|
897
|
+
self.logger.warning(f"on_time_change(nxt_on): {before_time} ~ {current_time} market is closed")
|
|
898
|
+
return
|
|
899
|
+
else:
|
|
900
|
+
self.logger.warning(f"on_time_change: {before_time} ~ {current_time} market is closed")
|
|
901
|
+
return
|
|
786
902
|
|
|
787
903
|
for order in list(self.pending_orders):
|
|
788
904
|
if self.time_unit == "minutes":
|
|
@@ -794,7 +910,6 @@ class MockBroker(BaseBroker):
|
|
|
794
910
|
# 강제로 1분 차이를 만들어 줌
|
|
795
911
|
before_time = current_time - dtm.timedelta(seconds=60)
|
|
796
912
|
|
|
797
|
-
# TODO: exchange에 따라 다른 봉을 보는 처리가 추가로 되어야 함
|
|
798
913
|
df = self.get_minute_price(order.asset_code)
|
|
799
914
|
if df.empty:
|
|
800
915
|
self.logger.warning(f"ORDER CANCELED: {order.asset_code} minute data is empty")
|
|
@@ -76,7 +76,9 @@ class BacktestEnvironment(TradingEnvironment):
|
|
|
76
76
|
time_unit: str = "minutes",
|
|
77
77
|
position_provider: BasePositionProvider = None,
|
|
78
78
|
market: str = "kr_stock",
|
|
79
|
-
current_data_handling: Literal["include", "virtual", "exclude"] = "virtual"
|
|
79
|
+
current_data_handling: Literal["include", "virtual", "exclude"] = "virtual",
|
|
80
|
+
local_cache_path: str = "./data",
|
|
81
|
+
market_nxt_on: bool = False,
|
|
80
82
|
):
|
|
81
83
|
"""BacktestEnvironment 클래스의 초기화 메서드입니다.
|
|
82
84
|
|
|
@@ -94,6 +96,8 @@ class BacktestEnvironment(TradingEnvironment):
|
|
|
94
96
|
- "include": 현재(분)까지의 실제 데이터 반환
|
|
95
97
|
- "virtual": 직전(1분전)까지의 데이터 + 현재(분)의 시가로 통일한 가상 데이터 반환 (기본값)
|
|
96
98
|
- "exclude": 직전(1분전)까지의 데이터만 반환
|
|
99
|
+
local_cache_path (str, optional): 백테스트 사용 데이터 로컬 캐시 경로. Defaults to "./data".
|
|
100
|
+
market_nxt_on (bool, optional): NXT 시장 처리 여부. Defaults to False.
|
|
97
101
|
|
|
98
102
|
Raises:
|
|
99
103
|
AssertionError: start_time이 end_time보다 늦거나,
|
|
@@ -107,7 +111,7 @@ class BacktestEnvironment(TradingEnvironment):
|
|
|
107
111
|
tzinfo = None if market == "kr_stock" else ZoneInfo("America/New_York")
|
|
108
112
|
|
|
109
113
|
self.clock = WallClock(live_mode=False, start_time=start_time, end_time=end_time, tzinfo=tzinfo)
|
|
110
|
-
self.broker = MockBroker(self.clock, position_provider, market, time_unit, current_data_handling)
|
|
114
|
+
self.broker = MockBroker(self.clock, position_provider, market, time_unit, current_data_handling, local_cache_path=local_cache_path, market_nxt_on=market_nxt_on)
|
|
111
115
|
|
|
112
116
|
|
|
113
117
|
class KISDomesticEnvironment(TradingEnvironment):
|
|
@@ -138,7 +142,7 @@ class KISDomesticEnvironment(TradingEnvironment):
|
|
|
138
142
|
```
|
|
139
143
|
"""
|
|
140
144
|
|
|
141
|
-
def __init__(self, paper_trading: bool = False):
|
|
145
|
+
def __init__(self, paper_trading: bool = False, market_nxt_on: bool = False):
|
|
142
146
|
"""KISDomesticEnvironment 클래스의 초기화 메서드입니다.
|
|
143
147
|
|
|
144
148
|
Args:
|
|
@@ -155,6 +159,7 @@ class KISDomesticEnvironment(TradingEnvironment):
|
|
|
155
159
|
data_api=conn.broker_simple,
|
|
156
160
|
trading_api=conn.broker_simple if not paper_trading else conn.paper_broker_simple,
|
|
157
161
|
clock=self.clock,
|
|
162
|
+
market_nxt_on=market_nxt_on,
|
|
158
163
|
)
|
|
159
164
|
|
|
160
165
|
|
|
@@ -219,7 +219,7 @@ class KISDomesticStock:
|
|
|
219
219
|
]
|
|
220
220
|
|
|
221
221
|
for k in date_keys:
|
|
222
|
-
if k in output and len(output[k]) > 0:
|
|
222
|
+
if k in output and len(output[k]) > 0 and output[k] != '0': # NXT 종목이 아닌데 지정한 경우 output[k] 값이 0
|
|
223
223
|
output[k] = dtm.datetime.strptime(output[k], "%Y%m%d").date()
|
|
224
224
|
|
|
225
225
|
result = {"rt_cd": res_body["rt_cd"], "msg_cd": res_body["msg_cd"], "msg1": res_body["msg1"], "output": output}
|
|
@@ -2646,6 +2646,16 @@ class KISDomesticStock:
|
|
|
2646
2646
|
- ocr_no: OCR번호
|
|
2647
2647
|
- crfd_item_yn: 크라우드펀딩종목여부
|
|
2648
2648
|
- elec_scty_yn: 전자증권여부
|
|
2649
|
+
- issu_istt_cd: 발행기관코드
|
|
2650
|
+
- etf_chas_erng_rt_dbnb: ETF추적수익율배수
|
|
2651
|
+
- etf_etn_ivst_heed_item_yn: ETF/ETN투자유의종목여부
|
|
2652
|
+
- stln_int_rt_dvsn_cd: 대주이자율구분코드
|
|
2653
|
+
- frnr_psnl_lmt_rt: 외국인개인한도비율
|
|
2654
|
+
- lstg_rqsr_issu_istt_cd: 상장신청인발행기관코드
|
|
2655
|
+
- lstg_rqsr_item_cd: 상장신청인종목코드
|
|
2656
|
+
- trst_istt_issu_istt_cd: 신탁기관발행기관코드
|
|
2657
|
+
- cptt_trad_tr_psbl_yn: NXT 거래종목여부
|
|
2658
|
+
- nxt_tr_stop_yn: NXT 거래정지여부
|
|
2649
2659
|
|
|
2650
2660
|
Raise:
|
|
2651
2661
|
ValueError: API 에러 발생시
|
|
@@ -4,7 +4,7 @@ from pyqqq.brokerage.kis.oauth import KISAuth
|
|
|
4
4
|
from pyqqq.data.realtime import get_all_last_trades
|
|
5
5
|
from pyqqq.datatypes import *
|
|
6
6
|
from pyqqq.utils.logger import get_logger
|
|
7
|
-
from pyqqq.utils.market_schedule import get_market_schedule
|
|
7
|
+
from pyqqq.utils.market_schedule import get_market_schedule, get_last_trading_day
|
|
8
8
|
from pyqqq.utils.mock_api import with_mock
|
|
9
9
|
from typing import AsyncGenerator, Dict, List, Optional
|
|
10
10
|
import asyncio
|
|
@@ -263,8 +263,9 @@ class KISSimpleDomesticStock:
|
|
|
263
263
|
result.extend(chunk)
|
|
264
264
|
|
|
265
265
|
df = pd.DataFrame(result)
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
if not df.empty:
|
|
267
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
268
|
+
df.set_index("date", inplace=True)
|
|
268
269
|
|
|
269
270
|
return df
|
|
270
271
|
|
|
@@ -278,16 +279,23 @@ class KISSimpleDomesticStock:
|
|
|
278
279
|
|
|
279
280
|
Args:
|
|
280
281
|
asset_code(str): 종목코드
|
|
281
|
-
data_exchange(DataExchange): 데이터 거래소
|
|
282
|
+
data_exchange(DataExchange): 데이터 거래소 (cf. NXT의 경우 해당되지 않는 종목은 Empty DataFrame이 아니고 모든 값이 0인 DataFrame이 반환됩니다.)
|
|
282
283
|
|
|
283
284
|
Returns:
|
|
284
|
-
pd.DataFrame: 분봉 데이터
|
|
285
|
+
pd.DataFrame: 분봉 데이터 (시간의 역순)
|
|
285
286
|
"""
|
|
286
287
|
|
|
287
288
|
request_datetime = dtm.datetime.now()
|
|
288
289
|
request_time = request_datetime.replace(second=0, microsecond=0)
|
|
289
290
|
result = []
|
|
290
291
|
schedule = get_market_schedule(dtm.date.today())
|
|
292
|
+
if schedule.full_day_closed:
|
|
293
|
+
_last_day = get_last_trading_day()
|
|
294
|
+
schedule = get_market_schedule(_last_day)
|
|
295
|
+
if data_exchange == DataExchange.NXT:
|
|
296
|
+
request_time = dtm.datetime.combine(_last_day, dtm.time(20, 0, 0))
|
|
297
|
+
else:
|
|
298
|
+
request_time = dtm.datetime.combine(_last_day, schedule.close_time)
|
|
291
299
|
|
|
292
300
|
while True:
|
|
293
301
|
r = self.stock_api.inquire_time_itemchartprice(
|
|
@@ -321,9 +329,9 @@ class KISSimpleDomesticStock:
|
|
|
321
329
|
|
|
322
330
|
open_time = schedule.open_time
|
|
323
331
|
|
|
324
|
-
# FIXME: 넥스트레이드
|
|
332
|
+
# FIXME: 넥스트레이드 시장 (수능일 같은 경우 넥스트레이드는 시간 변동 없이 8시 그대로 오픈한다는 얘기가 있어서 확인 필요)
|
|
325
333
|
if data_exchange == DataExchange.NXT:
|
|
326
|
-
open_time =
|
|
334
|
+
open_time = dtm.time(8, 0, 0)
|
|
327
335
|
|
|
328
336
|
if request_time.time() < open_time:
|
|
329
337
|
break
|
|
@@ -1110,6 +1118,8 @@ class KISSimpleDomesticStock:
|
|
|
1110
1118
|
r = self.stock_api.inquire_asking_price_exp_ccn(asset_code, self._get_data_exchange(data_exchange))
|
|
1111
1119
|
|
|
1112
1120
|
o1 = r["output1"]
|
|
1121
|
+
if not o1:
|
|
1122
|
+
return {}
|
|
1113
1123
|
|
|
1114
1124
|
result = {
|
|
1115
1125
|
"total_bid_volume": o1["total_bidp_rsqn"],
|
|
@@ -149,11 +149,11 @@ def get_all_day_data(
|
|
|
149
149
|
source (str, optional): 데이터를 검색할 API. 'ebest' 또는 'kis'를 지정할 수 있습니다. 기본값은 'ebest'입니다.
|
|
150
150
|
adjusted (bool): 수정주가 여부. 기본값은 True.
|
|
151
151
|
ascending (bool): 오름차순 여부. 기본값은 True.
|
|
152
|
-
exchange (Union[str, DataExchange]): 거래소. 기본값은 KRX.
|
|
152
|
+
exchange (Union[str, DataExchange]): 거래소. 기본값은 KRX. (cf. NXT의 경우 해당되지 않는 종목은 Empty DataFrame이 반환됩니다.)
|
|
153
153
|
|
|
154
154
|
Returns:
|
|
155
155
|
dict[str, pd.DataFrame]: 주식 코드를 키로 하고, 해당 주식의 일일 OHLCV 데이터가 포함된 pandas DataFrame을 값으로 하는 딕셔너리.
|
|
156
|
-
각 DataFrame에는 변환된 'time' 열이 포함되어 있으며, 이는 조회된 데이터의 시간을 나타냅니다. 'time' 열은 DataFrame의 인덱스로
|
|
156
|
+
각 DataFrame에는 변환된 'time' 열이 포함되어 있으며, 이는 조회된 데이터의 시간을 나타냅니다. 'time' 열은 DataFrame의 인덱스로 설정되고 오름차순으로 정렬됩니다.
|
|
157
157
|
|
|
158
158
|
DataFrame의 열은 다음과 같습니다:
|
|
159
159
|
|
|
@@ -228,7 +228,7 @@ def _get_nxt_schedule(date: datetime.date) -> MarketSchedule:
|
|
|
228
228
|
return schedule
|
|
229
229
|
|
|
230
230
|
|
|
231
|
-
def get_last_trading_day(date: datetime.date = None, exchange: Union[str, Exchange] = "KRX") -> datetime.date:
|
|
231
|
+
def get_last_trading_day(date: Optional[datetime.date] = None, exchange: Union[str, Exchange] = "KRX") -> datetime.date:
|
|
232
232
|
"""
|
|
233
233
|
주어진 날짜의 이전 거래일을 반환합니다.
|
|
234
234
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|