pyqqq 0.12.186__tar.gz → 0.12.188__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 (58) hide show
  1. {pyqqq-0.12.186 → pyqqq-0.12.188}/PKG-INFO +1 -1
  2. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyproject.toml +1 -1
  3. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/backtest/broker.py +93 -28
  4. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/kis/domestic_stock.py +5 -2
  5. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/tracker.py +0 -8
  6. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/data/daily.py +36 -24
  7. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/data/realtime.py +33 -6
  8. {pyqqq-0.12.186 → pyqqq-0.12.188}/README.md +0 -0
  9. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/__init__.py +0 -0
  10. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/backtest/__init__.py +0 -0
  11. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/backtest/environment.py +0 -0
  12. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/backtest/logger.py +0 -0
  13. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/backtest/positionprovider.py +0 -0
  14. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/backtest/strategy.py +0 -0
  15. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/backtest/utils.py +0 -0
  16. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/backtest/wallclock.py +0 -0
  17. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/__init__.py +0 -0
  18. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/ebest/__init__.py +0 -0
  19. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/ebest/domestic_stock.py +0 -0
  20. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/ebest/oauth.py +0 -0
  21. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/ebest/simple.py +0 -0
  22. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/ebest/tr_client.py +0 -0
  23. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/helper.py +0 -0
  24. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/kis/__init__.py +0 -0
  25. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/kis/oauth.py +0 -0
  26. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/kis/overseas_stock.py +0 -0
  27. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/kis/simple.py +0 -0
  28. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/kis/simple_overseas.py +0 -0
  29. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/kis/tr_client.py +0 -0
  30. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/brokerage/multiprocess_tracker.py +0 -0
  31. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/config.py +0 -0
  32. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/data/__init__.py +0 -0
  33. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/data/domestic.py +0 -0
  34. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/data/index.py +0 -0
  35. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/data/minutes.py +0 -0
  36. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/data/overseas.py +0 -0
  37. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/data/ticks.py +0 -0
  38. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/data/us_stocks.py +0 -0
  39. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/datatypes.py +0 -0
  40. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/executors/__init__.py +0 -0
  41. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/executors/hook.py +0 -0
  42. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/__init__.py +0 -0
  43. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/api_client.py +0 -0
  44. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/array.py +0 -0
  45. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/casting.py +0 -0
  46. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/compute.py +0 -0
  47. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/copycat.py +0 -0
  48. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/daily_tickers.py +0 -0
  49. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/display.py +0 -0
  50. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/kvstore.py +0 -0
  51. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/limiter.py +0 -0
  52. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/local_cache.py +0 -0
  53. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/logger.py +0 -0
  54. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/market_schedule.py +0 -0
  55. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/mock_api.py +0 -0
  56. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/position_classifier.py +0 -0
  57. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/retry.py +0 -0
  58. {pyqqq-0.12.186 → pyqqq-0.12.188}/pyqqq/utils/singleton.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyqqq
3
- Version: 0.12.186
3
+ Version: 0.12.188
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.186"
3
+ version = "0.12.188"
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"
@@ -287,8 +287,8 @@ class TradingBroker(BaseBroker):
287
287
  if data_exchange == DataExchange.NXT and price_data.get("open_price", 0) == 0: # current_price는 어제의 값을 반환하므로 open_price가 0인지로 NXT 시장 여부 확인
288
288
  price_data = self.data_api.get_price(code, data_exchange=DataExchange.KRX)
289
289
  else:
290
- if self.market_nxt_on: # TODO: 정규장에서도 NXT 가격을 우선 조회하는 것이니 수정 필요
291
- price_data = self.data_api.get_price(code, data_exchange=DataExchange.NXT)
290
+ if self.market_nxt_on:
291
+ price_data = self.data_api.get_price(code, data_exchange=DataExchange.UN)
292
292
  if price_data.get("open_price", 0) == 0: # current_price는 어제의 값을 반환하므로 open_price가 0인지로 NXT 시장 여부 확인
293
293
  price_data = self.data_api.get_price(code, data_exchange=DataExchange.KRX)
294
294
  else:
@@ -306,7 +306,7 @@ class TradingBroker(BaseBroker):
306
306
  result = self.data_api.get_today_minute_data(code, data_exchange=DataExchange.KRX)
307
307
  else:
308
308
  if self.market_nxt_on:
309
- result = self.data_api.get_today_minute_data(code, data_exchange=DataExchange.NXT)
309
+ result = self.data_api.get_today_minute_data(code, data_exchange=DataExchange.UN)
310
310
  if result.empty or result.iloc[-1].close == 0:
311
311
  result = self.data_api.get_today_minute_data(code, data_exchange=DataExchange.KRX)
312
312
  else:
@@ -316,12 +316,12 @@ class TradingBroker(BaseBroker):
316
316
  def get_daily_price(self, code: str, from_date: dtm.date, to_date: dtm.date, data_exchange: Optional[DataExchange] = None) -> pd.DataFrame:
317
317
  if data_exchange:
318
318
  result = self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True, data_exchange=data_exchange)
319
- if data_exchange == DataExchange.NXT and result.empty:
319
+ if data_exchange == DataExchange.NXT and (result.empty or result.iloc[0].open == 0):
320
320
  result = self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True, data_exchange=DataExchange.KRX)
321
321
  else:
322
322
  if self.market_nxt_on:
323
- result = self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True, data_exchange=DataExchange.NXT)
324
- if result.empty:
323
+ result = self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True, data_exchange=DataExchange.UN)
324
+ if result.empty or result.iloc[0].open == 0:
325
325
  result = self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True, data_exchange=DataExchange.KRX)
326
326
  else:
327
327
  result = self.data_api.get_historical_daily_data(code, from_date, to_date, adjusted_price=True, data_exchange=DataExchange.KRX)
@@ -576,7 +576,12 @@ class MockBroker(BaseBroker):
576
576
  market_schedule = get_market_schedule(today, exchange=self._get_exchange_code())
577
577
  open_time = market_schedule.open_time
578
578
  if self.market_nxt_on:
579
- open_time = dtm.time(8, 0, 0)
579
+ try:
580
+ nxt_market_schedule = get_market_schedule(today, exchange="NXT")
581
+ open_time = nxt_market_schedule.open_time
582
+ except ValueError:
583
+ # 2025-03-04 이전 날짜는 ValueError 이므로 무시
584
+ pass
580
585
 
581
586
  if now.time() < open_time:
582
587
  last_trading_day = get_last_trading_day(today, exchange=self._get_exchange_code())
@@ -603,7 +608,7 @@ class MockBroker(BaseBroker):
603
608
 
604
609
  def get_minute_price(self, code: str) -> pd.DataFrame:
605
610
  """
606
- self.clock 기준 당일의 정규장 시작부터 현재 시각 (이전) 까지의 분봉 데이터를 조회합니다.
611
+ self.clock 기준 당일의 정규장 시작부터 현재 시각 (이전) 까지의 분봉 데이터를 조회합니다.
607
612
  단, market_nxt_on 이면, NXT 가능 종목의 경우 프리마켓, 정규마켓, 애프터마켓의 분봉을 모두 조회합니다.
608
613
 
609
614
  Args:
@@ -623,10 +628,15 @@ class MockBroker(BaseBroker):
623
628
  close_time = dtm.datetime.combine(today, market_schedule.close_time)
624
629
 
625
630
  # TODO: us_stock 에 대해서는 따로 적용해야 함.
626
- is_nxt = df.index[0] == dtm.datetime.combine(today, dtm.time(8, 0, 0))
627
- if self.market_nxt_on and is_nxt:
628
- open_time = dtm.datetime.combine(today, dtm.time(8, 0, 0))
629
- close_time = dtm.datetime.combine(today, dtm.time(20, 0, 0))
631
+ try:
632
+ nxt_market_schedule = get_market_schedule(today, "NXT")
633
+ is_nxt = df.index[0] == dtm.datetime.combine(today, nxt_market_schedule.open_time)
634
+ if self.market_nxt_on and is_nxt:
635
+ open_time = dtm.datetime.combine(today, nxt_market_schedule.open_time)
636
+ close_time = dtm.datetime.combine(today, nxt_market_schedule.close_time)
637
+ except ValueError:
638
+ # 2025-03-04 이전 날짜는 ValueError 이므로 무시
639
+ pass
630
640
 
631
641
  if df.index.tz is not None:
632
642
  df.index = df.index.tz_localize(None)
@@ -707,9 +717,40 @@ class MockBroker(BaseBroker):
707
717
  dfs = get_us_minute_data(date, [code])
708
718
  else:
709
719
  if self.market_nxt_on:
710
- dfs = get_kr_minute_data(date, [code], dtm.timedelta(minutes=1), source="kis", adjusted=True, exchange=DataExchange.NXT)
711
- if dfs[code].empty: # NXT 해당 종목이 없는 경우 KRX 의 결과값을 반환한다. 하지만 로컬 캐시 폴더는 NXT 기준으로 저장한다.
712
- dfs = get_kr_minute_data(date, [code], dtm.timedelta(minutes=1), source="kis", adjusted=True, exchange=DataExchange.KRX)
720
+ # NXT 해당 종목이 없는 경우 KRX 의 결과값을 반환한다. 하지만 로컬 캐시 폴더는 NXT 기준으로 저장한다.
721
+ dfs = get_kr_minute_data(date, [code], dtm.timedelta(minutes=1), source="kis", adjusted=True, exchange=DataExchange.KRX)
722
+ dfs_nxt = get_kr_minute_data(date, [code], dtm.timedelta(minutes=1), source="kis", adjusted=True, exchange=DataExchange.NXT)
723
+ if code in dfs_nxt and not dfs_nxt[code].empty:
724
+ _df = dfs[code]
725
+ _df_nxt = dfs_nxt[code]
726
+ common_dates = _df.index.intersection(_df_nxt.index)
727
+ for _date in common_dates:
728
+ _df_nxt.loc[_date, 'volume'] = _df.loc[_date, 'volume'] + _df_nxt.loc[_date, 'volume']
729
+ _df_nxt.loc[_date, 'value'] = _df.loc[_date, 'value'] + _df_nxt.loc[_date, 'value']
730
+ _df_nxt.loc[_date, 'cum_volume'] = _df.loc[_date, 'cum_volume'] + _df_nxt.loc[_date, 'cum_volume']
731
+ _df_nxt.loc[_date, 'cum_value'] = _df.loc[_date, 'cum_value'] + _df_nxt.loc[_date, 'cum_value']
732
+ # 한투 앱에서 통합 차트의 경우 KRX값이 우선으로 사용됨
733
+ _df_nxt.loc[_date, 'open'] = _df.loc[_date, 'open']
734
+ _df_nxt.loc[_date, 'high'] = max(_df.loc[_date, 'high'], _df_nxt.loc[_date, 'high'])
735
+ _df_nxt.loc[_date, 'low'] = min(_df.loc[_date, 'low'], _df_nxt.loc[_date, 'low'])
736
+ _df_nxt.loc[_date, 'close'] = _df.loc[_date, 'close']
737
+
738
+ # 15:30 이후의 cum_volume, cum_value는 15:30의 cum_volume, cum_value에다가 _df_nxt의 15:31 부터 각 volume, value를 매분 누적하여 저장
739
+ market_close_datetime = dtm.datetime.combine(date, get_market_schedule(date, "KRX").close_time)
740
+ if market_close_datetime in _df_nxt.index:
741
+ base_cum_volume = _df_nxt.loc[market_close_datetime, 'cum_volume']
742
+ base_cum_value = _df_nxt.loc[market_close_datetime, 'cum_value']
743
+
744
+ # 15:31 이후 데이터에 대해 누적 계산
745
+ after_market_close = _df_nxt.index > market_close_datetime
746
+ if after_market_close.any():
747
+ for idx in _df_nxt[after_market_close].index:
748
+ _df_nxt.loc[idx, 'cum_volume'] = base_cum_volume + _df_nxt.loc[idx, 'volume']
749
+ _df_nxt.loc[idx, 'cum_value'] = base_cum_value + _df_nxt.loc[idx, 'value']
750
+ base_cum_volume = _df_nxt.loc[idx, 'cum_volume']
751
+ base_cum_value = _df_nxt.loc[idx, 'cum_value']
752
+
753
+ dfs[code] = _df_nxt
713
754
  else:
714
755
  dfs = get_kr_minute_data(date, [code], dtm.timedelta(minutes=1), source="kis", adjusted=True, exchange=DataExchange.KRX)
715
756
 
@@ -736,7 +777,7 @@ class MockBroker(BaseBroker):
736
777
 
737
778
  def get_daily_price(self, code: str, from_date: Optional[dtm.date] = None, end_date: Optional[dtm.date] = None):
738
779
  """
739
- 지정된 기간 동안의 정규장 시작부터 현재 시각 (이전) 까지의 일봉 데이터를 조회합니다.
780
+ 지정된 기간 동안의 정규장 시작부터 현재 시각 (이전) 까지의 일봉 데이터를 조회합니다.
740
781
  단, market_nxt_on 이면, NXT 가능 종목의 경우 프리마켓, 정규마켓, 애프터마켓의 일봉을 모두 조회합니다.
741
782
  """
742
783
  today = self.clock.today()
@@ -758,9 +799,18 @@ class MockBroker(BaseBroker):
758
799
  dfs = get_us_daily_data([code], from_date, end_date, adjusted=True, ascending=True)
759
800
  else:
760
801
  if self.market_nxt_on:
761
- dfs = get_kr_daily_data([code], from_date, end_date, adjusted=True, ascending=True, exchange=DataExchange.NXT)
762
- if code not in dfs or dfs[code].empty:
763
- dfs = get_kr_daily_data([code], from_date, end_date, adjusted=True, ascending=True, exchange=DataExchange.KRX)
802
+ dfs = get_kr_daily_data([code], from_date, end_date, adjusted=True, ascending=True, exchange=DataExchange.KRX)
803
+ dfs_nxt = get_kr_daily_data([code], from_date, end_date, adjusted=True, ascending=True, exchange=DataExchange.NXT)
804
+ if code in dfs_nxt:
805
+ _df = dfs[code]
806
+ _df_nxt = dfs_nxt[code]
807
+
808
+ # 같은 날짜의 데이터만 volume과 value를 합치고, open, close는 NXT 값으로 교체
809
+ common_dates = _df.index.intersection(_df_nxt.index)
810
+ for _date in common_dates:
811
+ _df.loc[_date, 'volume'] = _df.loc[_date, 'volume'] + _df_nxt.loc[_date, 'volume']
812
+ _df.loc[_date, 'value'] = _df.loc[_date, 'value'] + _df_nxt.loc[_date, 'value']
813
+ _df.loc[_date, 'open'] = _df_nxt.loc[_date, 'open']
764
814
  else:
765
815
  dfs = get_kr_daily_data([code], from_date, end_date, adjusted=True, ascending=True, exchange=DataExchange.KRX)
766
816
 
@@ -773,8 +823,13 @@ class MockBroker(BaseBroker):
773
823
  open_time = schedule.open_time
774
824
  close_time = schedule.close_time
775
825
  if self.market_nxt_on:
776
- open_time = dtm.time(8, 0, 0)
777
- close_time = dtm.time(20, 0, 0)
826
+ try:
827
+ nxt_schedule = get_market_schedule(today, "NXT")
828
+ open_time = nxt_schedule.open_time
829
+ close_time = nxt_schedule.close_time
830
+ except ValueError:
831
+ # 2025-03-04 이전 날짜는 ValueError 이므로 무시
832
+ pass
778
833
 
779
834
  if schedule.full_day_closed:
780
835
  pass # end_date 가 폐장일이면 해당 날짜의 데이터는 이미 없음
@@ -933,18 +988,22 @@ class MockBroker(BaseBroker):
933
988
  self.get_positions() # self.positions 에 있는 포지션들의 현재 가격을 조회하여 업데이트 함
934
989
 
935
990
  today = current_time.date()
936
- market_schedule = get_market_schedule(today)
991
+ market_schedule = get_market_schedule(today, exchange=Exchange.KRX)
937
992
 
938
993
  open_time = dtm.datetime.combine(today, market_schedule.open_time)
939
994
  close_time = dtm.datetime.combine(today, market_schedule.close_time)
940
995
  preclose_auction_start_time = close_time - dtm.timedelta(minutes=10)
941
996
 
942
- nxt_pre_open_time = dtm.datetime.combine(today, dtm.time(8, 0, 0))
943
- nxt_pre_close_time = dtm.datetime.combine(today, dtm.time(8, 50, 0))
944
- # nxt_main_open_time = dtm.datetime.combine(today, dtm.time(9, 0, 30))
945
- # nxt_main_close_time = dtm.datetime.combine(today, dtm.time(15, 20, 0))
946
- nxt_after_open_time = dtm.datetime.combine(today, dtm.time(15, 40, 0))
947
- nxt_after_close_time = dtm.datetime.combine(today, dtm.time(20, 0, 0))
997
+ try:
998
+ nxt_market_schedule = get_market_schedule(today, exchange=Exchange.NXT)
999
+ except ValueError:
1000
+ nxt_market_schedule = market_schedule
1001
+ nxt_pre_open_time = dtm.datetime.combine(today, nxt_market_schedule.open_time)
1002
+ nxt_pre_close_time = nxt_pre_open_time + dtm.timedelta(minutes=50)
1003
+ # nxt_main_open_time = dtm.datetime.combine(today, open_time) + dtm.timedelta(seconds=30)
1004
+ # nxt_main_close_time = close_time - dtm.timedelta(minutes=10)
1005
+ nxt_after_open_time = close_time
1006
+ nxt_after_close_time = dtm.datetime.combine(today, nxt_market_schedule.close_time)
948
1007
 
949
1008
  just_closed = current_time.replace(second=0, microsecond=0) == close_time.replace(second=0, microsecond=0)
950
1009
  if preclose_auction_start_time - dtm.timedelta(minutes=1) <= current_time <= preclose_auction_start_time + dtm.timedelta(minutes=1):
@@ -971,6 +1030,10 @@ class MockBroker(BaseBroker):
971
1030
  # 강제로 1분 차이를 만들어 줌
972
1031
  before_time = current_time - dtm.timedelta(seconds=60)
973
1032
 
1033
+ if self.market_nxt_on and current_time < nxt_after_close_time and current_time - before_time < dtm.timedelta(seconds=60):
1034
+ # 강제로 1분 차이를 만들어 줌
1035
+ before_time = current_time - dtm.timedelta(seconds=60)
1036
+
974
1037
  df = self.get_minute_price(order.asset_code)
975
1038
  if df.empty:
976
1039
  self.logger.warning(f"ORDER CANCELED: {order.asset_code} minute data is empty")
@@ -1278,6 +1341,7 @@ class MockBroker(BaseBroker):
1278
1341
  d["market_cap"] = round(dft["value"].iloc[-1] / 100000000)
1279
1342
  except Exception as e:
1280
1343
  self.logger.error(f"get_market_cap error: {d['asset_code']} {d['name']} {prev_date} {e}")
1344
+ d["market_cap"] = 0
1281
1345
 
1282
1346
  # 전일 거래대금
1283
1347
  try:
@@ -1285,6 +1349,7 @@ class MockBroker(BaseBroker):
1285
1349
  d["prev_value"] = round(dft["value"].iloc[-1] / 100000000)
1286
1350
  except Exception as e:
1287
1351
  self.logger.error(f"전일거래대금 error: {d['asset_code']} {d['name']} {prev_date} {e}")
1352
+ d["prev_value"] = 0
1288
1353
  dict_list.append(d)
1289
1354
 
1290
1355
  df = pd.DataFrame(dict_list)
@@ -3477,7 +3477,7 @@ class KISDomesticStock:
3477
3477
  self,
3478
3478
  cano: str,
3479
3479
  acnt_prdt_cd: str,
3480
- inqr_dvsn_1: str = "1",
3480
+ inqr_dvsn_1: str = "0",
3481
3481
  inqr_dvsn_2: str = "0",
3482
3482
  ctx_area_fk100: str = "",
3483
3483
  ctx_area_nk100: str = "",
@@ -3490,7 +3490,7 @@ class KISDomesticStock:
3490
3490
  Args:
3491
3491
  cano (str): 종합계좌번호 - 계좌번호 체계(8-2)의 앞 8자리
3492
3492
  acnt_prdt_cd (str): 계좌상품코드 - 계좌번호 체계(8-2)의 뒤 2자리
3493
- inqr_dvsn_1 (str): 조회구분1 - 0:조회순서 1:주문순 2:종목순
3493
+ inqr_dvsn_1 (str): 조회구분1 - 0:주문 1:종목
3494
3494
  inqr_dvsn_2 (str): 조회구분2 - 0:전체 1:매도 2:매수
3495
3495
  ctx_area_fk100 (str): CTX_AREA_FK100
3496
3496
  ctx_area_nk100 (str): CTX_AREA_NK100
@@ -4644,6 +4644,9 @@ class KISDomesticStock:
4644
4644
 
4645
4645
  한국투자 HTS(eFriend Plus) > [0856] 기간별 매매손익 화면 에서 "일별" 클릭 시의 기능을 API로 개발한 사항으로, 해당 화면을 참고하시면 기능을 이해하기 쉽습니다.
4646
4646
 
4647
+ Note:
4648
+ - 00:30 ~ 03:00 사이에는 조회 불가능합니다.
4649
+
4647
4650
  Args:
4648
4651
  acnt_prdt_cd (str): 계좌상품코드
4649
4652
  cano (str): 종합계좌번호
@@ -474,15 +474,7 @@ class TradingTracker:
474
474
  self.positions = self.simple_api.get_positions()
475
475
 
476
476
  def _refresh_pending_orders(self):
477
- old_pending_orders = {}
478
- for o in self.pending_orders:
479
- old_pending_orders[o.order_no] = o
480
-
481
477
  self.pending_orders = self.simple_api.get_pending_orders(exchanges=list(OrderExchange))
482
- for o in self.pending_orders:
483
- old_pending_order = old_pending_orders.get(o.order_no)
484
- if old_pending_order is not None:
485
- o.average_purchase_price = old_pending_order.average_purchase_price
486
478
 
487
479
  def _notify_pending_order_update(self, status: str, order: StockOrder):
488
480
  # deprecated
@@ -46,23 +46,26 @@ def get_all_ohlcv_for_date(
46
46
  - close (int): 종가.
47
47
  - volume (int): 거래량.
48
48
  - value (int): 거래대금.
49
- - diff (int): 종가 대비 전일 종가의 차이.
50
- - diff_rate (float): 종가 대비 전일 종가의 변화율.
51
49
 
52
50
  Raises:
53
51
  HTTPError: API 요청이 실패했을 때 발생.
54
52
 
55
53
  Examples:
56
- >>> ohlcv_data = get_all_ohlcv_for_date(datetime.date(2023, 5, 8))
54
+ >>> ohlcv_data = get_all_ohlcv_for_date(datetime.date(2025, 3, 4), exchange="KRX")
57
55
  >>> print(ohlcv_data)
58
- open high low close volume value diff diff_rate
56
+ open high low close volume value
59
57
  code
60
- 000020 8710 8790 8710 8770 39019 341233350 60 0.69
61
- 000040 1052 1133 1047 1047 590401 632158688 8 0.77
62
- 000050 7740 7870 7700 7750 1445 11211730 10 0.13
63
- 000070 68300 68800 67400 67600 33358 2261622200 -800 -1.17
64
- 000075 54800 54900 54800 54800 177 9702400 -100 -0.18
65
- ...
58
+ 000020 6200 6220 6090 6130 41954 257785065
59
+ 000040 422 422 393 400 135979 54618038
60
+ 000050 6350 6350 6240 6290 9957 62588010
61
+ 000070 59800 61100 59500 60400 32555 1957081250
62
+ 000075 59000 59400 58300 59400 2600 151987900
63
+ ... ... ... ... ... ... ...
64
+ 950160 35500 39150 35500 38950 332462 12638215825
65
+ 950170 4900 4955 4880 4900 24198 118688025
66
+ 950190 7860 7920 7770 7860 21309 166408215
67
+ 950200 3855 3855 3785 3800 23268 88458215
68
+ 950220 996 996 945 954 440017 421451605
66
69
  """
67
70
  if isinstance(date, datetime.datetime):
68
71
  date = date.date()
@@ -89,10 +92,15 @@ def get_all_ohlcv_for_date(
89
92
  return pd.DataFrame()
90
93
  else:
91
94
  df = pd.DataFrame(rows, columns=cols)
92
- df = df.astype({"open": int, "high": int, "low": int, "close": int, "volume": int, "value": int, "diff": int, "diff_rate": float})
93
- df.drop(columns=["date"], inplace=True)
95
+
96
+ # NOTE 서버 응답에서 diff, diff_rate 가 제거될 예정
97
+ columns_to_drop = ["diff", "diff_rate", "date"]
98
+ df.drop(columns=[col for col in columns_to_drop if col in df.columns], inplace=True)
94
99
  df.set_index("code", inplace=True)
95
100
 
101
+ df = df[["open", "high", "low", "close", "volume", "value"]]
102
+ df = df.astype({"open": int, "high": int, "low": int, "close": int, "volume": int, "value": int})
103
+
96
104
  return df
97
105
 
98
106
 
@@ -133,25 +141,23 @@ def get_ohlcv_by_codes_for_period(
133
141
  - close (int): 종가.
134
142
  - volume (int): 거래량.
135
143
  - value (int): 거래대금.
136
- - diff (int): 종가 대비 전일 종가의 차이.
137
- - diff_rate (float): 종가 대비 전일 종가의 변화율.
138
144
 
139
145
  Raises:
140
146
  HTTPError: API 요청이 실패했을 때 발생.
141
147
 
142
148
  Examples:
143
- >>> dfs = get_ohlcv_by_codes_for_period(['005930', '319640'], datetime.date(2024, 5, 7), datetime.date(2024, 5, 9))
149
+ >>> dfs = get_ohlcv_by_codes_for_period(['005930', '319640'], datetime.date(2025, 7, 28), datetime.date(2025, 7, 30))
144
150
  >>> print(dfs)
145
- {'319640': open high low close volume value diff diff_rate
151
+ {'319640': open high low close volume value
146
152
  date
147
- 2024-05-09 15380 15445 15365 15430 16200 249577480 -5 -0.03
148
- 2024-05-08 15445 15460 15365 15435 6419 99007660 -5 -0.03
149
- 2024-05-07 15355 15525 15355 15440 22318 345280470 85 0.55,
150
- '005930': open high low close volume value diff diff_rate
153
+ 2025-07-30 20860 20930 20840 20870 47431 990834244
154
+ 2025-07-29 20795 20850 20765 20810 39627 824529926
155
+ 2025-07-28 20900 20985 20880 20965 26610 556764707,
156
+ '005930': open high low close volume value
151
157
  date
152
- 2024-05-09 81100 81500 79700 79700 18700919 1504404274500 -1600 -1.97
153
- 2024-05-08 80800 81400 80500 81300 12960682 1050108654400 0 0.00
154
- 2024-05-07 79600 81300 79400 81300 26238868 2112619288066 3700 4.77}
158
+ 2025-07-30 71000 73700 70600 72600 34761444 2521446314798
159
+ 2025-07-29 70800 70800 68800 70600 28190940 1977721049950
160
+ 2025-07-28 68200 70400 67200 70400 35332500 2431396606750}
155
161
  """
156
162
  exchange = DataExchange.validate(exchange)
157
163
  tz = pytz.timezone("Asia/Seoul")
@@ -192,8 +198,14 @@ def get_ohlcv_by_codes_for_period(
192
198
  rows.reverse()
193
199
 
194
200
  df = pd.DataFrame(rows, columns=cols)
195
- df = df.astype({"open": int, "high": int, "low": int, "close": int, "volume": int, "value": int, "diff": int, "diff_rate": float})
201
+
202
+ # NOTE 서버 응답에서 diff, diff_rate 가 제거될 예정
203
+ columns_to_drop = ["diff", "diff_rate"]
204
+ df.drop(columns=[col for col in columns_to_drop if col in df.columns], inplace=True)
196
205
  df.set_index("date", inplace=True)
206
+
207
+ df = df[["open", "high", "low", "close", "volume", "value"]]
208
+ df = df.astype({"open": int, "high": int, "low": int, "close": int, "volume": int, "value": int})
197
209
  df.sort_index(ascending=ascending, inplace=True)
198
210
  result[code] = df
199
211
 
@@ -8,10 +8,11 @@ from typing import List, Union
8
8
  import websockets
9
9
 
10
10
  import pyqqq.config as c
11
- from pyqqq.datatypes import DataExchange
11
+ from pyqqq.datatypes import DataExchange, Exchange
12
12
  from pyqqq.utils.api_client import raise_for_status, send_request
13
13
  from pyqqq.utils.logger import get_logger
14
14
  from pyqqq.utils.singleton import singleton
15
+ from pyqqq.utils.market_schedule import is_trading_time, get_next_trading_day, get_market_schedule
15
16
 
16
17
  logger = get_logger(__name__)
17
18
 
@@ -136,6 +137,7 @@ class TickEventListener:
136
137
  self.retry_cnt = 0
137
138
  self.ws = None
138
139
  self.tot_retry_cnt = 0
140
+ self.reconnect_datetime = None
139
141
  logger.info(f"{self.LOG_TAG}init")
140
142
 
141
143
  if "event_add_delay" in config:
@@ -143,12 +145,12 @@ class TickEventListener:
143
145
 
144
146
  if "health_ping_delay" in config:
145
147
  self.health_ping_delay = config["health_ping_delay"]
146
-
148
+ now_int_str = str(int(dtm.datetime.now().timestamp()))
147
149
  if client_id is not None:
148
150
  api_key = os.getenv("PYQQQ_API_KEY") or ""
149
- self.client_id = client_id + api_key
151
+ self.client_id = client_id + api_key + "_" + now_int_str
150
152
  else:
151
- self.client_id = api_key
153
+ self.client_id = api_key + "_" + now_int_str
152
154
 
153
155
  self.url = url
154
156
  if self.url is None:
@@ -186,13 +188,37 @@ class TickEventListener:
186
188
  async for message in websocket:
187
189
  # logger.debug(f"{self.LOG_TAG}message: {message}")
188
190
  await self.handle_response(message)
191
+
192
+ logger.warning(f"{self.LOG_TAG}Websocket connection closed by server. code: {websocket.close_code}, reason: {websocket.close_reason}")
189
193
  except websockets.exceptions.ConnectionClosed:
190
194
  logger.info(f"{self.LOG_TAG}Connection closed")
191
- await self.on_connect_failed()
195
+ if websocket.close_code == 4409:
196
+ logger.info(f"{self.LOG_TAG}Connection closed by server with code: {websocket.close_code}, reason: {websocket.close_reason}")
197
+
198
+ self.move_events_to_pending()
199
+
200
+ if is_trading_time(dtm.datetime.now()):
201
+ logger.info("try reconnect next minute")
202
+ self.reconnect_datetime = dtm.datetime.now() + dtm.timedelta(minutes=1)
203
+ else:
204
+ logger.info("not trading time")
205
+ next_trading_day = get_next_trading_day(dtm.datetime.now().date())
206
+ next_market_schedule = get_market_schedule(next_trading_day, Exchange.NXT)
207
+ next_start_time = dtm.datetime.combine(next_trading_day, next_market_schedule.open_time)
208
+ logger.info(f"reconnect at {next_start_time}")
209
+ self.reconnect_datetime = next_start_time
210
+
211
+ await asyncio.sleep(self.reconnect_datetime.timestamp() - dtm.datetime.now().timestamp())
212
+
213
+ await self.connect_ws()
214
+
215
+ else:
216
+ await self.on_connect_failed()
192
217
  except Exception as e:
193
218
  logger.info(f"{self.LOG_TAG}Connection failed. error: {e}")
194
219
  await self.on_connect_failed()
195
220
  finally:
221
+ logger.info(f"{self.LOG_TAG}Websocket connection closed")
196
222
  self.ws_connected = False
197
223
 
198
224
  async def health_ping(self):
@@ -321,7 +347,8 @@ class TickEventListener:
321
347
  logger.debug(f"{self.LOG_TAG} try connect events:{cur_pending_event_ids}, connected: {res}")
322
348
 
323
349
  else:
324
- logger.debug(f"{self.LOG_TAG}not connected")
350
+ logger.info(f"{self.LOG_TAG} not connected. stop process_events_appending")
351
+ return
325
352
 
326
353
  await asyncio.sleep(self.event_add_delay)
327
354
 
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