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.

Files changed (63) hide show
  1. {pyqqq-0.12.168 → pyqqq-0.12.170}/PKG-INFO +1 -1
  2. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyproject.toml +1 -1
  3. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/broker.py +153 -38
  4. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/environment.py +8 -3
  5. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/domestic_stock.py +11 -1
  6. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/simple.py +17 -7
  7. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/daily.py +3 -0
  8. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/minutes.py +2 -2
  9. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/realtime.py +1 -0
  10. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/market_schedule.py +1 -1
  11. {pyqqq-0.12.168 → pyqqq-0.12.170}/README.md +0 -0
  12. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/__init__.py +0 -0
  13. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/ai/__init__.py +0 -0
  14. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/ai/daily.py +0 -0
  15. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/ai/domestic.py +0 -0
  16. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/ai/market_schedule.py +0 -0
  17. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/ai/minute.py +0 -0
  18. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/__init__.py +0 -0
  19. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/logger.py +0 -0
  20. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/positionprovider.py +0 -0
  21. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/strategy.py +0 -0
  22. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/utils.py +0 -0
  23. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/backtest/wallclock.py +0 -0
  24. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/__init__.py +0 -0
  25. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/ebest/__init__.py +0 -0
  26. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/ebest/domestic_stock.py +0 -0
  27. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/ebest/oauth.py +0 -0
  28. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/ebest/simple.py +0 -0
  29. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/ebest/tr_client.py +0 -0
  30. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/helper.py +0 -0
  31. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/__init__.py +0 -0
  32. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/oauth.py +0 -0
  33. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/overseas_stock.py +0 -0
  34. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/simple_overseas.py +0 -0
  35. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/kis/tr_client.py +0 -0
  36. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/multiprocess_tracker.py +0 -0
  37. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/brokerage/tracker.py +0 -0
  38. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/config.py +0 -0
  39. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/__init__.py +0 -0
  40. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/domestic.py +0 -0
  41. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/index.py +0 -0
  42. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/overseas.py +0 -0
  43. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/ticks.py +0 -0
  44. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/data/us_stocks.py +0 -0
  45. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/datatypes.py +0 -0
  46. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/executors/__init__.py +0 -0
  47. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/executors/hook.py +0 -0
  48. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/__init__.py +0 -0
  49. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/api_client.py +0 -0
  50. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/array.py +0 -0
  51. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/casting.py +0 -0
  52. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/compute.py +0 -0
  53. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/copycat.py +0 -0
  54. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/daily_tickers.py +0 -0
  55. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/display.py +0 -0
  56. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/kvstore.py +0 -0
  57. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/limiter.py +0 -0
  58. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/local_cache.py +0 -0
  59. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/logger.py +0 -0
  60. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/mock_api.py +0 -0
  61. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/position_classifier.py +0 -0
  62. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/retry.py +0 -0
  63. {pyqqq-0.12.168 → pyqqq-0.12.170}/pyqqq/utils/singleton.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyqqq
3
- Version: 0.12.168
3
+ Version: 0.12.170
4
4
  Summary: Package for quantitative strategy development on the PyQQQ platform
5
5
  License: MIT
6
6
  Author: PyQQQ team
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyqqq"
3
- version = "0.12.168"
3
+ version = "0.12.170"
4
4
  description = "Package for quantitative strategy development on the PyQQQ platform"
5
5
  authors = ["PyQQQ team <pyqqq.cs@gmail.com>"]
6
6
  readme = "README.md"
@@ -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
- price_data = self.data_api.get_price(code)
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
- return self.data_api.get_today_minute_data(code)
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
- return self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True)
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
- return self.data_api.get_orderbook()
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
- schedule = get_market_schedule(today, exchange=self._get_exchange_code())
474
- if now.time() < schedule.open_time:
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
- file_path = f"{self.local_cache_path}/minutes/{date}/{code}.csv" if self.local_cache_path else None
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
- df = pd.read_csv(file_path)
564
- df["time"] = pd.to_datetime(df["time"])
565
- df.set_index("time", inplace=True)
566
- return df
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.market == "us_stock":
570
- dfs = get_us_minute_data(date, [code])
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
- df = dfs[code]
575
- if df.empty:
576
- return df
656
+ df = dfs[code]
657
+ if df.empty:
658
+ return df
577
659
 
578
- if self.local_cache_path:
579
- if not os.path.exists(f"{self.local_cache_path}/minutes/{date}"):
580
- os.makedirs(f"{self.local_cache_path}/minutes/{date}")
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
- df.reset_index(inplace=True)
583
- df.to_csv(file_path, index=False)
584
- df.set_index("time", inplace=True)
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
- return df
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
- dfs = get_kr_daily_data([code], from_date, end_date, adjusted=True, ascending=True)
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() < schedule.open_time:
725
+ if now.time() < open_time:
622
726
  # 장 오픈 전은 전일 데이터 까지만 반환
623
727
  df = _exclude_today(df)
624
- elif now.time() <= schedule.close_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.logger.warning(f"on_time_change: {before_time} ~ {current_time} market is closed")
785
- return
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
- df["date"] = pd.to_datetime(df["date"])
267
- df.set_index("date", inplace=True)
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: 넥스트레이드 시장은 1시간 데이터를 조회해야
332
+ # FIXME: 넥스트레이드 시장 (수능일 같은 경우 넥스트레이드는 시간 변동 없이 8시 그대로 오픈한다는 얘기가 있어서 확인 필요)
325
333
  if data_exchange == DataExchange.NXT:
326
- open_time = (dtm.datetime.combine(request_datetime.date(), schedule.open_time) - dtm.timedelta(hours=1)).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"],
@@ -177,6 +177,9 @@ def get_ohlcv_by_codes_for_period(
177
177
  cols = data["cols"]
178
178
  dataset = data["rows"]
179
179
 
180
+ if not dataset:
181
+ continue
182
+
180
183
  for code in dataset.keys():
181
184
  rows = dataset[code]
182
185
  for row in rows:
@@ -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
 
@@ -95,6 +95,7 @@ def get_all_last_orderbooks():
95
95
 
96
96
  return result
97
97
 
98
+
98
99
  @singleton
99
100
  class TickEventListener:
100
101
  """
@@ -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