FinMind 2.0.8__tar.gz → 2.0.10__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.
Files changed (60) hide show
  1. {finmind-2.0.8 → finmind-2.0.10}/FinMind/data/data_loader.py +33 -0
  2. {finmind-2.0.8 → finmind-2.0.10}/FinMind/data/finmind_api.py +17 -10
  3. {finmind-2.0.8 → finmind-2.0.10}/FinMind/schema/data.py +1 -0
  4. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/base.py +18 -11
  5. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/utils.py +11 -2
  6. finmind-2.0.10/FinMind/utility/request.py +277 -0
  7. {finmind-2.0.8/finmind.egg-info → finmind-2.0.10}/PKG-INFO +13 -7
  8. {finmind-2.0.8 → finmind-2.0.10}/README.md +12 -6
  9. {finmind-2.0.8 → finmind-2.0.10/finmind.egg-info}/PKG-INFO +13 -7
  10. finmind-2.0.8/FinMind/utility/request.py +0 -156
  11. {finmind-2.0.8 → finmind-2.0.10}/FinMind/__init__.py +0 -0
  12. {finmind-2.0.8 → finmind-2.0.10}/FinMind/_version.py +0 -0
  13. {finmind-2.0.8 → finmind-2.0.10}/FinMind/crawler/__init__.py +0 -0
  14. {finmind-2.0.8 → finmind-2.0.10}/FinMind/crawler/base.py +0 -0
  15. {finmind-2.0.8 → finmind-2.0.10}/FinMind/crawler/commodities.py +0 -0
  16. {finmind-2.0.8 → finmind-2.0.10}/FinMind/crawler/demo.py +0 -0
  17. {finmind-2.0.8 → finmind-2.0.10}/FinMind/crawler/government_bonds.py +0 -0
  18. {finmind-2.0.8 → finmind-2.0.10}/FinMind/data/__init__.py +0 -0
  19. {finmind-2.0.8 → finmind-2.0.10}/FinMind/data/data_subscriber.py +0 -0
  20. {finmind-2.0.8 → finmind-2.0.10}/FinMind/indicators/__init__.py +0 -0
  21. {finmind-2.0.8 → finmind-2.0.10}/FinMind/indicators/bias.py +0 -0
  22. {finmind-2.0.8 → finmind-2.0.10}/FinMind/indicators/continue_holding.py +0 -0
  23. {finmind-2.0.8 → finmind-2.0.10}/FinMind/indicators/institutional_investors_follower.py +0 -0
  24. {finmind-2.0.8 → finmind-2.0.10}/FinMind/indicators/institutional_investors_over_buy.py +0 -0
  25. {finmind-2.0.8 → finmind-2.0.10}/FinMind/indicators/kd.py +0 -0
  26. {finmind-2.0.8 → finmind-2.0.10}/FinMind/indicators/kd_crossover.py +0 -0
  27. {finmind-2.0.8 → finmind-2.0.10}/FinMind/indicators/ma_cross_orver.py +0 -0
  28. {finmind-2.0.8 → finmind-2.0.10}/FinMind/indicators/short_sale_margin_purchase_ratio.py +0 -0
  29. {finmind-2.0.8 → finmind-2.0.10}/FinMind/plotting/__init__.py +0 -0
  30. {finmind-2.0.8 → finmind-2.0.10}/FinMind/plotting/bar.py +0 -0
  31. {finmind-2.0.8 → finmind-2.0.10}/FinMind/plotting/kline.py +0 -0
  32. {finmind-2.0.8 → finmind-2.0.10}/FinMind/plotting/line.py +0 -0
  33. {finmind-2.0.8 → finmind-2.0.10}/FinMind/plotting/pie.py +0 -0
  34. {finmind-2.0.8 → finmind-2.0.10}/FinMind/schema/__init__.py +0 -0
  35. {finmind-2.0.8 → finmind-2.0.10}/FinMind/schema/indicators.py +0 -0
  36. {finmind-2.0.8 → finmind-2.0.10}/FinMind/schema/info.py +0 -0
  37. {finmind-2.0.8 → finmind-2.0.10}/FinMind/schema/plot.py +0 -0
  38. {finmind-2.0.8 → finmind-2.0.10}/FinMind/schema/rule.py +0 -0
  39. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/__init__.py +0 -0
  40. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/bias.py +0 -0
  41. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/continue_holding.py +0 -0
  42. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/institutional_investors_follower.py +0 -0
  43. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/kd.py +0 -0
  44. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/kd_crossover.py +0 -0
  45. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/ma_crossover.py +0 -0
  46. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/macd_crossover.py +0 -0
  47. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/max_min_period_bias.py +0 -0
  48. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/naive_kd.py +0 -0
  49. {finmind-2.0.8 → finmind-2.0.10}/FinMind/strategies/short_sale_margin_purchase_ratio.py +0 -0
  50. {finmind-2.0.8 → finmind-2.0.10}/FinMind/templates/post.html +0 -0
  51. {finmind-2.0.8 → finmind-2.0.10}/FinMind/utility/__init__.py +0 -0
  52. {finmind-2.0.8 → finmind-2.0.10}/FinMind/utility/rule.py +0 -0
  53. {finmind-2.0.8 → finmind-2.0.10}/LICENSE +0 -0
  54. {finmind-2.0.8 → finmind-2.0.10}/finmind.egg-info/SOURCES.txt +0 -0
  55. {finmind-2.0.8 → finmind-2.0.10}/finmind.egg-info/dependency_links.txt +0 -0
  56. {finmind-2.0.8 → finmind-2.0.10}/finmind.egg-info/requires.txt +0 -0
  57. {finmind-2.0.8 → finmind-2.0.10}/finmind.egg-info/top_level.txt +0 -0
  58. {finmind-2.0.8 → finmind-2.0.10}/pyproject.toml +0 -0
  59. {finmind-2.0.8 → finmind-2.0.10}/setup.cfg +0 -0
  60. {finmind-2.0.8 → finmind-2.0.10}/setup.py +0 -0
@@ -1217,6 +1217,38 @@ class DataLoader(FinMindApi):
1217
1217
  )
1218
1218
  return futures_daily
1219
1219
 
1220
+ def taiwan_futures_kbar(
1221
+ self,
1222
+ futures_id: str = "",
1223
+ date: str = "",
1224
+ timeout: int = None,
1225
+ ) -> pd.DataFrame:
1226
+ """get 期貨分K資料
1227
+ :param futures_id: 期貨代號("TX")
1228
+ :param date (str): 日期("2024-01-02")
1229
+ :param timeout (int): timeout seconds, default None
1230
+
1231
+ :return: 期貨分K TaiwanFuturesKBar
1232
+ :rtype pd.DataFrame
1233
+ :rtype column date (str): 日期
1234
+ :rtype column futures_id (str): 期貨代碼
1235
+ :rtype column contract_date (str): 到期月份
1236
+ :rtype column minute (str): 分鐘時間
1237
+ :rtype column open (float): 開盤價
1238
+ :rtype column high (float): 最高價
1239
+ :rtype column low (float): 最低價
1240
+ :rtype column close (float): 收盤價
1241
+ :rtype column volume (int): 成交量
1242
+ """
1243
+ futures_kbar = self.get_data(
1244
+ dataset=Dataset.TaiwanFuturesKBar,
1245
+ data_id=futures_id,
1246
+ start_date=date,
1247
+ end_date=date,
1248
+ timeout=timeout,
1249
+ )
1250
+ return futures_kbar
1251
+
1220
1252
  def taiwan_option_daily(
1221
1253
  self,
1222
1254
  option_id: str = "",
@@ -2088,6 +2120,7 @@ class DataLoader(FinMindApi):
2088
2120
  use_object: bool = False,
2089
2121
  ) -> pd.DataFrame:
2090
2122
  """get 台股分 K 資料表
2123
+ :param stock_id (str): 股票代碼, 加權指數請帶 "TAIEX"
2091
2124
  :param timeout (int): timeout seconds, default None
2092
2125
  :param use_object (bool): 是否透過 signed URL 下載整日 parquet 資料物件,
2093
2126
  設為 True 時忽略 stock_id, stock_id_list, use_async 參數, default False
@@ -7,7 +7,11 @@ import requests
7
7
  from loguru import logger
8
8
 
9
9
  from FinMind.schema.data import Dataset
10
- from FinMind.utility.request import async_request_get, request_get
10
+ from FinMind.utility.request import (
11
+ async_request_get,
12
+ download_storage_object,
13
+ request_get,
14
+ )
11
15
 
12
16
  logger.remove()
13
17
  logger.add(sys.stderr, level="INFO")
@@ -398,11 +402,19 @@ class FinMindApi:
398
402
  data = pd.DataFrame(self._extract_data(data))
399
403
  return data
400
404
 
401
- def get_object(self, dataset: str, date: str, timeout: int) -> pd.DataFrame:
405
+ def get_object(
406
+ self,
407
+ dataset: str,
408
+ date: str,
409
+ timeout: int,
410
+ max_retry_times: int = 10,
411
+ ) -> pd.DataFrame:
402
412
  """透過 signed URL 下載整日資料物件 (parquet 格式)
413
+ 傳輸中斷時會自動重試,並以 HTTP Range 從中斷處續傳
403
414
  :param dataset (str): 資料集名稱 (e.g. "TaiwanStockPriceTick")
404
415
  :param date (str): 資料日期 ("2025-01-06")
405
416
  :param timeout (int): request timeout 秒數
417
+ :param max_retry_times (int): 最多重試次數
406
418
 
407
419
  :return: 該日期所有資料
408
420
  :rtype: pd.DataFrame
@@ -412,16 +424,11 @@ class FinMindApi:
412
424
  "dataset": dataset,
413
425
  "date": date,
414
426
  }
415
- response = request_get(
427
+ content = download_storage_object(
416
428
  self.__session,
417
429
  url,
418
430
  params=params,
419
431
  timeout=timeout,
432
+ max_retry_times=max_retry_times,
420
433
  )
421
- status_code = response.status_code
422
- if status_code == 200:
423
- return pd.read_parquet(io.BytesIO(response.content))
424
- else:
425
- resp_json = response.json()
426
- logger.info(resp_json)
427
- return pd.DataFrame()
434
+ return pd.read_parquet(io.BytesIO(content))
@@ -30,6 +30,7 @@ class Dataset(str, Enum):
30
30
  TaiwanOptionTick = "TaiwanOptionTick"
31
31
  TaiwanStockPriceBidAsk = "TaiwanStockPriceBidAsk"
32
32
  TaiwanFuturesDaily = "TaiwanFuturesDaily"
33
+ TaiwanFuturesKBar = "TaiwanFuturesKBar"
33
34
  TaiwanOptionDaily = "TaiwanOptionDaily"
34
35
  TaiwanStockNews = "TaiwanStockNews"
35
36
  TaiwanStockTotalReturnIndex = "TaiwanStockTotalReturnIndex"
@@ -116,11 +116,12 @@ class Trader:
116
116
  self.UnrealizedProfit = capital_gains - sell_fee - sell_tax
117
117
  self.EverytimeProfit = self.UnrealizedProfit + self.RealizedProfit
118
118
 
119
- @staticmethod
120
119
  def __have_enough_money(
121
- trader_fund: int, trade_price: float, trade_volume: float
120
+ self, trader_fund: int, trade_price: float, trade_volume: float
122
121
  ) -> bool:
123
- return trader_fund >= (trade_price * trade_volume)
122
+ buy_price = trade_price * trade_volume
123
+ buy_fee = max(20.0, buy_price * self.fee)
124
+ return trader_fund >= (buy_price + buy_fee)
124
125
 
125
126
  @staticmethod
126
127
  def __have_enough_volume(hold_volume: float, trade_volume: float) -> bool:
@@ -266,7 +267,7 @@ class BackTest:
266
267
  indicators_info: typing.Union[IndicatorsInfo, typing.Dict[str, str]],
267
268
  ):
268
269
  indicators_info = (
269
- indicators_info.dict()
270
+ indicators_info.model_dump()
270
271
  if isinstance(indicators_info, IndicatorsInfo)
271
272
  else indicators_info
272
273
  )
@@ -323,7 +324,7 @@ class BackTest:
323
324
  ],
324
325
  ):
325
326
  return [
326
- rule.dict() if isinstance(rule, AddBuySellRule) else rule
327
+ rule.model_dump() if isinstance(rule, AddBuySellRule) else rule
327
328
  for rule in rule_list
328
329
  ]
329
330
 
@@ -584,11 +585,15 @@ class BackTest:
584
585
  gain_cash = (
585
586
  cash_div * trader.hold_volume + gain_stock_frac * 10
586
587
  ) # 將小數部分加進 gain_cash
588
+ # 配股「除權」不是一筆交易,總持有成本不會改變,
589
+ # 只是同一筆成本攤到更多股數上(每股成本被稀釋)。
590
+ # 因此 origin_cost 必須在 hold_volume 增加「之前」計算,
591
+ # 否則總成本會憑空跟著配股一起變大。
592
+ origin_cost = trader.hold_cost * trader.hold_volume
587
593
  trader.hold_volume += gain_stock_div
588
594
  # 在 UnrealizedProfit & RealizedProfit
589
595
  # 避免重複計算 gain_cash
590
- origin_cost = trader.hold_cost * trader.hold_volume
591
- # 持有成本不變,將配息歸類在,已實現損益
596
+ # 現金股利不調整持有成本,改歸類在已實現損益
592
597
  # new_cost = origin_cost - gain_cash
593
598
  trader.hold_cost = (
594
599
  origin_cost / trader.hold_volume if trader.hold_volume != 0 else 0
@@ -698,7 +703,7 @@ class BackTest:
698
703
  @property
699
704
  def final_stats(self) -> pd.Series:
700
705
  self._final_stats = pd.Series(
701
- FinalStats(**self._final_stats.to_dict()).dict()
706
+ FinalStats(**self._final_stats.to_dict()).model_dump()
702
707
  )
703
708
  return self._final_stats
704
709
 
@@ -706,7 +711,7 @@ class BackTest:
706
711
  def trade_detail(self) -> pd.DataFrame:
707
712
  self._trade_detail = pd.DataFrame(
708
713
  [
709
- TradeDetail(**row_dict).dict()
714
+ TradeDetail(**row_dict).model_dump()
710
715
  for row_dict in self._trade_detail.to_dict("records")
711
716
  ]
712
717
  )
@@ -716,7 +721,7 @@ class BackTest:
716
721
  def compare_market_detail(self) -> pd.DataFrame:
717
722
  self._compare_market_detail = pd.DataFrame(
718
723
  [
719
- CompareMarketDetail(**row_dict).dict()
724
+ CompareMarketDetail(**row_dict).model_dump()
720
725
  for row_dict in self._compare_market_detail.to_dict("records")
721
726
  ]
722
727
  )
@@ -725,7 +730,9 @@ class BackTest:
725
730
  @property
726
731
  def compare_market_stats(self) -> pd.Series:
727
732
  self._compare_market_stats = pd.Series(
728
- CompareMarketStats(**self._compare_market_stats.to_dict()).dict()
733
+ CompareMarketStats(
734
+ **self._compare_market_stats.to_dict()
735
+ ).model_dump()
729
736
  )
730
737
  return self._compare_market_stats
731
738
 
@@ -35,9 +35,18 @@ def get_asset_underlying_type(stock_id: str, data_loader: DataLoader) -> str:
35
35
  return sorted(industry_category.values)[0]
36
36
 
37
37
 
38
+ # ETF 的證交稅是 0.1%,一般股票是 0.3%。TaiwanStockInfo 對上櫃 ETF 的
39
+ # industry_category 有「上櫃指數股票型基金(ETF)」與「上櫃ETF」兩種寫法,
40
+ # 同一檔可能只出現其中一種,兩種都要對應到 ETF 稅率。
41
+ UNDERLYING_TRADING_TAX = {
42
+ "ETF": 0.001,
43
+ "上櫃指數股票型基金(ETF)": 0.001,
44
+ "上櫃ETF": 0.001,
45
+ }
46
+
47
+
38
48
  def get_underlying_trading_tax(underlying_type: str) -> float:
39
- mapping = {"ETF": 0.001, "上櫃指數股票型基金(ETF)": 0.001}
40
- return mapping.get(underlying_type, 0.003)
49
+ return UNDERLYING_TRADING_TAX.get(underlying_type, 0.003)
41
50
 
42
51
 
43
52
  def calculate_datenbr(day1: str, day2: str) -> int:
@@ -0,0 +1,277 @@
1
+ import asyncio
2
+ import concurrent.futures
3
+ import hashlib
4
+ import os
5
+ import ssl
6
+ import time
7
+ from typing import Dict, List, Union
8
+
9
+ import nest_asyncio
10
+ import requests
11
+ from loguru import logger
12
+ from tqdm import tqdm
13
+
14
+ nest_asyncio.apply()
15
+
16
+
17
+ def request_get(
18
+ session: requests.Session,
19
+ url: str,
20
+ params: Dict[str, Union[int, str, float]] = None,
21
+ timeout: int = 60,
22
+ max_retry_times: int = 10,
23
+ verbose: bool = False,
24
+ ):
25
+ """
26
+ 單次 request,支援 retry 與 log
27
+ """
28
+ response = None
29
+ for retry_times in range(1, max_retry_times + 1):
30
+ try:
31
+ response = session.get(
32
+ url,
33
+ verify=True,
34
+ params=params,
35
+ timeout=timeout,
36
+ )
37
+ if response.status_code == 504:
38
+ if verbose:
39
+ logger.warning(
40
+ f"status_code=504, retry {retry_times}/{max_retry_times}"
41
+ )
42
+ time.sleep(retry_times * 0.1)
43
+ else:
44
+ break
45
+ except (
46
+ requests.ConnectionError,
47
+ ssl.SSLError,
48
+ requests.exceptions.ReadTimeout,
49
+ requests.exceptions.ChunkedEncodingError,
50
+ ) as exc:
51
+ if verbose:
52
+ logger.warning(
53
+ f"Error: {exc}, retry {retry_times}/{max_retry_times}"
54
+ )
55
+ time.sleep(retry_times * 0.1)
56
+ except Exception as exc:
57
+ if verbose:
58
+ logger.warning(
59
+ f"Unexpected error: {exc}, retry {retry_times}/{max_retry_times}"
60
+ )
61
+ time.sleep(retry_times * 0.1)
62
+ if response and response.status_code != 200:
63
+ raise Exception(
64
+ f"Final response status: {response.status_code}, text: {response.text}"
65
+ )
66
+ return response
67
+
68
+
69
+ RETRYABLE_ERRORS = (
70
+ requests.ConnectionError,
71
+ ssl.SSLError,
72
+ requests.exceptions.ReadTimeout,
73
+ requests.exceptions.ChunkedEncodingError,
74
+ )
75
+ REDIRECT_STATUS = (301, 302, 303, 307, 308)
76
+
77
+
78
+ def download_storage_object(
79
+ session: requests.Session,
80
+ url: str,
81
+ params: Dict[str, Union[int, str, float]] = None,
82
+ timeout: int = 60,
83
+ max_retry_times: int = 10,
84
+ verbose: bool = False,
85
+ ) -> bytes:
86
+ """
87
+ 下載整日資料物件,傳輸中斷時以 Range + If-Match 續傳
88
+
89
+ storage_objects 回 307 導向有效期限約 3 分鐘的下載網址,
90
+ 所以每次(含重試)都重新取得網址;已收到的內容保留,
91
+ 只要求剩下的位元組,並以 If-Match 確認仍是同一版本,
92
+ 版本不同(412)才從頭下載。完成後檢查長度,ETag 不含 "-" 時再比對 MD5。
93
+ """
94
+ buffer = bytearray()
95
+ etag = None
96
+ total = None
97
+ for retry_times in range(1, max_retry_times + 1):
98
+ try:
99
+ response = session.get(
100
+ url,
101
+ verify=True,
102
+ params=params,
103
+ timeout=timeout,
104
+ allow_redirects=False,
105
+ )
106
+ except RETRYABLE_ERRORS as exc:
107
+ if verbose:
108
+ logger.warning(
109
+ f"Error: {exc}, retry {retry_times}/{max_retry_times}"
110
+ )
111
+ time.sleep(retry_times * 0.5)
112
+ continue
113
+ if response.status_code == 200:
114
+ return response.content
115
+ if response.status_code == 504:
116
+ time.sleep(retry_times * 0.5)
117
+ continue
118
+ if response.status_code not in REDIRECT_STATUS:
119
+ raise Exception(
120
+ f"Final response status: {response.status_code}, text: {response.text}"
121
+ )
122
+ object_url = response.headers["Location"]
123
+
124
+ headers = {}
125
+ if buffer and etag:
126
+ headers = {"Range": f"bytes={len(buffer)}-", "If-Match": etag}
127
+ try:
128
+ # 下載網址本身帶簽章,不能帶 session 的 Authorization header
129
+ with requests.get(
130
+ object_url, headers=headers, stream=True, timeout=timeout
131
+ ) as object_response:
132
+ status_code = object_response.status_code
133
+ if status_code == 200:
134
+ buffer = bytearray()
135
+ etag = object_response.headers.get("ETag")
136
+ content_length = object_response.headers.get(
137
+ "Content-Length"
138
+ )
139
+ total = int(content_length) if content_length else None
140
+ elif status_code == 412:
141
+ # 物件在兩次請求之間已更新,已收到的內容作廢
142
+ buffer, etag, total = bytearray(), None, None
143
+ continue
144
+ elif status_code != 206:
145
+ if verbose:
146
+ logger.warning(
147
+ f"status_code={status_code}, retry {retry_times}/{max_retry_times}"
148
+ )
149
+ time.sleep(retry_times * 0.5)
150
+ continue
151
+ for chunk in object_response.iter_content(chunk_size=1 << 20):
152
+ buffer.extend(chunk)
153
+ except RETRYABLE_ERRORS as exc:
154
+ if verbose:
155
+ logger.warning(
156
+ f"Download interrupted at {len(buffer)} bytes: {exc}, "
157
+ f"retry {retry_times}/{max_retry_times}"
158
+ )
159
+ time.sleep(retry_times * 0.5)
160
+ continue
161
+
162
+ if total is not None and len(buffer) < total:
163
+ continue
164
+ too_long = total is not None and len(buffer) > total
165
+ if too_long or not _match_etag(buffer, etag):
166
+ if verbose:
167
+ logger.warning(
168
+ f"Downloaded object failed verification, retry {retry_times}/{max_retry_times}"
169
+ )
170
+ buffer, etag, total = bytearray(), None, None
171
+ continue
172
+ return bytes(buffer)
173
+ raise Exception(
174
+ f"Download storage object failed after {max_retry_times} retries: "
175
+ f"received {len(buffer)} of {total} bytes"
176
+ )
177
+
178
+
179
+ def _match_etag(content: bytearray, etag: str) -> bool:
180
+ """ETag 不含 "-" 時等於整個物件的 MD5;分段上傳的 ETag 無法以 MD5 驗證"""
181
+ if not etag:
182
+ return True
183
+ etag = etag.strip('"')
184
+ if "-" in etag:
185
+ return True
186
+ return hashlib.md5(content).hexdigest() == etag
187
+
188
+
189
+ async def _loop_run_get(
190
+ executor: concurrent.futures.ThreadPoolExecutor,
191
+ loop: asyncio.AbstractEventLoop,
192
+ session: requests.Session,
193
+ url: str,
194
+ params: Dict[str, Union[str, int, float]],
195
+ timeout: int = 60,
196
+ max_retry_times: int = 10,
197
+ verbose: bool = False,
198
+ ):
199
+ """
200
+ 將同步 request 包成 async
201
+ """
202
+ return await loop.run_in_executor(
203
+ executor,
204
+ request_get,
205
+ session,
206
+ url,
207
+ params,
208
+ timeout,
209
+ max_retry_times,
210
+ verbose,
211
+ )
212
+
213
+
214
+ def async_request_get(
215
+ session: requests.Session,
216
+ url: str,
217
+ params_list: List[Dict[str, Union[str, int, float]]],
218
+ timeout: int = 60,
219
+ max_retry_times: int = 10,
220
+ verbose: bool = False,
221
+ auto_tune: bool = True,
222
+ max_concurrency: int = None,
223
+ batch_size: int = 10,
224
+ ):
225
+ """
226
+ 批量 async request,支援自動根據機器資源調整 max_concurrency / batch_size
227
+ """
228
+ # 自動調整
229
+ if auto_tune:
230
+ cpu_count = os.cpu_count() or 1
231
+ max_concurrency = max_concurrency or max(1, cpu_count * 2)
232
+ else:
233
+ max_concurrency = max_concurrency or 10
234
+
235
+ async def runner():
236
+ semaphore = asyncio.Semaphore(max_concurrency)
237
+ loop = asyncio.get_event_loop()
238
+ executor = concurrent.futures.ThreadPoolExecutor(
239
+ max_workers=max_concurrency
240
+ )
241
+
242
+ async def _limited_run(params):
243
+ async with semaphore:
244
+ resp = await _loop_run_get(
245
+ executor,
246
+ loop,
247
+ session,
248
+ url=url,
249
+ params=params,
250
+ timeout=timeout,
251
+ max_retry_times=max_retry_times,
252
+ verbose=verbose,
253
+ )
254
+ return resp
255
+
256
+ results = []
257
+ pbar = tqdm(total=len(params_list))
258
+
259
+ # 分 batch 建立 task
260
+ for i in range(0, len(params_list), batch_size):
261
+ batch = params_list[i : i + batch_size]
262
+ tasks = [asyncio.create_task(_limited_run(p)) for p in batch]
263
+
264
+ for coro in asyncio.as_completed(tasks):
265
+ try:
266
+ res = await coro
267
+ results.append(res)
268
+ except Exception as exc:
269
+ if verbose:
270
+ logger.error(f"Task failed: {exc}")
271
+ pbar.update(1)
272
+
273
+ pbar.close()
274
+ executor.shutdown()
275
+ return results
276
+
277
+ return asyncio.run(runner())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finmind
3
- Version: 2.0.8
3
+ Version: 2.0.10
4
4
  Summary: financial mining
5
5
  Author-email: linsam <finmind.tw@gmail.com>
6
6
  License: Apache License
@@ -245,8 +245,8 @@ Dynamic: license-file
245
245
  </p>
246
246
 
247
247
  <p align="center">
248
- <a href="https://travis-ci.org/FinMind/FinMind"><img src="https://travis-ci.org/FinMind/FinMind.svg?branch=master" alt="Build Status"></a>
249
- <a href="https://github.com/linsamtw/FinMind/blob/master/LICENSE"><img src="https://img.shields.io/github/license/FinMind/FinMind" alt="license"></a>
248
+ <a href="https://github.com/FinMind/FinMind/actions/workflows/python-package.yml"><img src="https://github.com/FinMind/FinMind/actions/workflows/python-package.yml/badge.svg?branch=master" alt="Build Status"></a>
249
+ <a href="https://github.com/FinMind/FinMind/blob/master/LICENSE"><img src="https://img.shields.io/github/license/FinMind/FinMind" alt="license"></a>
250
250
  <a href="https://finmind.github.io/"><img src="https://readthedocs.org/projects/finminddoc/badge/?version=latest" alt="Documentation Status"></a>
251
251
  <a href="https://gitter.im/FinMindTW/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge"><img src="https://badges.gitter.im/FinMindTW/community.svg" alt="Gitter"></a>
252
252
  <a href="https://badge.fury.io/py/FinMind"><img src="https://badge.fury.io/py/FinMind.svg" alt="PyPI version"></a>
@@ -285,7 +285,7 @@ pip install FinMind
285
285
 
286
286
  ## 這是什麼?
287
287
 
288
- **FinMind** 是超過 50 種金融開源數據 [50 datasets](https://finmind.github.io/)。
288
+ **FinMind** 提供台灣與國際市場的金融開源數據,完整資料集清單請見[官方文件](https://finmind.github.io/)。
289
289
  包含
290
290
 
291
291
  * 技術面 : 台股股價 daily、即時報價、歷史 tick、PER、PBR、每5秒委託成交統計、加權指數、當日沖銷交易標的及成交量值。
@@ -299,8 +299,9 @@ pip install FinMind
299
299
 
300
300
  ## What is this?
301
301
 
302
- **FinMind** is open source of more
303
- than [50 datasets](https://finmind.github.io/), including
302
+ **FinMind** provides open financial datasets for Taiwan and international
303
+ markets. See the [official documentation](https://finmind.github.io/) for the
304
+ current dataset list, including
304
305
 
305
306
  Taiwan stock trade data daily, Taiwan stock trade data (5 seconds) (2019-05-29 ~
306
307
  now, more than 30 million data in total), Financial Statements, Balance Sheet,
@@ -321,7 +322,7 @@ without having to collect the data by yourself.
321
322
 
322
323
  ## License
323
324
 
324
- - [License Detail](https://github.com/linsamtw/FinMind/blob/master/LICENSE)
325
+ - [License Detail](https://github.com/FinMind/FinMind/blob/master/LICENSE)
325
326
 
326
327
  - 本專案提供的所有內容均用於教育、非商業用途。資料僅供參考,使用者依本資料交易發生交易損失需自行負責,本專案不對資料內容錯誤、更新延誤或傳輸中斷負任何責任。
327
328
 
@@ -390,6 +391,11 @@ Email: FinMind.TW@gmail.com
390
391
 
391
392
  每週日早上零點至早上七點為維護時間,不提供服務。
392
393
 
394
+ ## Contributing
395
+
396
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, test commands,
397
+ and pull request checklist.
398
+
393
399
  ## Note
394
400
 
395
401
  + 未來預計新增更多功能,包含個人化回測分析、chatbot 監控策略,
@@ -3,8 +3,8 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <a href="https://travis-ci.org/FinMind/FinMind"><img src="https://travis-ci.org/FinMind/FinMind.svg?branch=master" alt="Build Status"></a>
7
- <a href="https://github.com/linsamtw/FinMind/blob/master/LICENSE"><img src="https://img.shields.io/github/license/FinMind/FinMind" alt="license"></a>
6
+ <a href="https://github.com/FinMind/FinMind/actions/workflows/python-package.yml"><img src="https://github.com/FinMind/FinMind/actions/workflows/python-package.yml/badge.svg?branch=master" alt="Build Status"></a>
7
+ <a href="https://github.com/FinMind/FinMind/blob/master/LICENSE"><img src="https://img.shields.io/github/license/FinMind/FinMind" alt="license"></a>
8
8
  <a href="https://finmind.github.io/"><img src="https://readthedocs.org/projects/finminddoc/badge/?version=latest" alt="Documentation Status"></a>
9
9
  <a href="https://gitter.im/FinMindTW/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge"><img src="https://badges.gitter.im/FinMindTW/community.svg" alt="Gitter"></a>
10
10
  <a href="https://badge.fury.io/py/FinMind"><img src="https://badge.fury.io/py/FinMind.svg" alt="PyPI version"></a>
@@ -43,7 +43,7 @@ pip install FinMind
43
43
 
44
44
  ## 這是什麼?
45
45
 
46
- **FinMind** 是超過 50 種金融開源數據 [50 datasets](https://finmind.github.io/)。
46
+ **FinMind** 提供台灣與國際市場的金融開源數據,完整資料集清單請見[官方文件](https://finmind.github.io/)。
47
47
  包含
48
48
 
49
49
  * 技術面 : 台股股價 daily、即時報價、歷史 tick、PER、PBR、每5秒委託成交統計、加權指數、當日沖銷交易標的及成交量值。
@@ -57,8 +57,9 @@ pip install FinMind
57
57
 
58
58
  ## What is this?
59
59
 
60
- **FinMind** is open source of more
61
- than [50 datasets](https://finmind.github.io/), including
60
+ **FinMind** provides open financial datasets for Taiwan and international
61
+ markets. See the [official documentation](https://finmind.github.io/) for the
62
+ current dataset list, including
62
63
 
63
64
  Taiwan stock trade data daily, Taiwan stock trade data (5 seconds) (2019-05-29 ~
64
65
  now, more than 30 million data in total), Financial Statements, Balance Sheet,
@@ -79,7 +80,7 @@ without having to collect the data by yourself.
79
80
 
80
81
  ## License
81
82
 
82
- - [License Detail](https://github.com/linsamtw/FinMind/blob/master/LICENSE)
83
+ - [License Detail](https://github.com/FinMind/FinMind/blob/master/LICENSE)
83
84
 
84
85
  - 本專案提供的所有內容均用於教育、非商業用途。資料僅供參考,使用者依本資料交易發生交易損失需自行負責,本專案不對資料內容錯誤、更新延誤或傳輸中斷負任何責任。
85
86
 
@@ -148,6 +149,11 @@ Email: FinMind.TW@gmail.com
148
149
 
149
150
  每週日早上零點至早上七點為維護時間,不提供服務。
150
151
 
152
+ ## Contributing
153
+
154
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, test commands,
155
+ and pull request checklist.
156
+
151
157
  ## Note
152
158
 
153
159
  + 未來預計新增更多功能,包含個人化回測分析、chatbot 監控策略,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finmind
3
- Version: 2.0.8
3
+ Version: 2.0.10
4
4
  Summary: financial mining
5
5
  Author-email: linsam <finmind.tw@gmail.com>
6
6
  License: Apache License
@@ -245,8 +245,8 @@ Dynamic: license-file
245
245
  </p>
246
246
 
247
247
  <p align="center">
248
- <a href="https://travis-ci.org/FinMind/FinMind"><img src="https://travis-ci.org/FinMind/FinMind.svg?branch=master" alt="Build Status"></a>
249
- <a href="https://github.com/linsamtw/FinMind/blob/master/LICENSE"><img src="https://img.shields.io/github/license/FinMind/FinMind" alt="license"></a>
248
+ <a href="https://github.com/FinMind/FinMind/actions/workflows/python-package.yml"><img src="https://github.com/FinMind/FinMind/actions/workflows/python-package.yml/badge.svg?branch=master" alt="Build Status"></a>
249
+ <a href="https://github.com/FinMind/FinMind/blob/master/LICENSE"><img src="https://img.shields.io/github/license/FinMind/FinMind" alt="license"></a>
250
250
  <a href="https://finmind.github.io/"><img src="https://readthedocs.org/projects/finminddoc/badge/?version=latest" alt="Documentation Status"></a>
251
251
  <a href="https://gitter.im/FinMindTW/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge"><img src="https://badges.gitter.im/FinMindTW/community.svg" alt="Gitter"></a>
252
252
  <a href="https://badge.fury.io/py/FinMind"><img src="https://badge.fury.io/py/FinMind.svg" alt="PyPI version"></a>
@@ -285,7 +285,7 @@ pip install FinMind
285
285
 
286
286
  ## 這是什麼?
287
287
 
288
- **FinMind** 是超過 50 種金融開源數據 [50 datasets](https://finmind.github.io/)。
288
+ **FinMind** 提供台灣與國際市場的金融開源數據,完整資料集清單請見[官方文件](https://finmind.github.io/)。
289
289
  包含
290
290
 
291
291
  * 技術面 : 台股股價 daily、即時報價、歷史 tick、PER、PBR、每5秒委託成交統計、加權指數、當日沖銷交易標的及成交量值。
@@ -299,8 +299,9 @@ pip install FinMind
299
299
 
300
300
  ## What is this?
301
301
 
302
- **FinMind** is open source of more
303
- than [50 datasets](https://finmind.github.io/), including
302
+ **FinMind** provides open financial datasets for Taiwan and international
303
+ markets. See the [official documentation](https://finmind.github.io/) for the
304
+ current dataset list, including
304
305
 
305
306
  Taiwan stock trade data daily, Taiwan stock trade data (5 seconds) (2019-05-29 ~
306
307
  now, more than 30 million data in total), Financial Statements, Balance Sheet,
@@ -321,7 +322,7 @@ without having to collect the data by yourself.
321
322
 
322
323
  ## License
323
324
 
324
- - [License Detail](https://github.com/linsamtw/FinMind/blob/master/LICENSE)
325
+ - [License Detail](https://github.com/FinMind/FinMind/blob/master/LICENSE)
325
326
 
326
327
  - 本專案提供的所有內容均用於教育、非商業用途。資料僅供參考,使用者依本資料交易發生交易損失需自行負責,本專案不對資料內容錯誤、更新延誤或傳輸中斷負任何責任。
327
328
 
@@ -390,6 +391,11 @@ Email: FinMind.TW@gmail.com
390
391
 
391
392
  每週日早上零點至早上七點為維護時間,不提供服務。
392
393
 
394
+ ## Contributing
395
+
396
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, test commands,
397
+ and pull request checklist.
398
+
393
399
  ## Note
394
400
 
395
401
  + 未來預計新增更多功能,包含個人化回測分析、chatbot 監控策略,
@@ -1,156 +0,0 @@
1
- import asyncio
2
- import concurrent.futures
3
- import os
4
- import ssl
5
- import time
6
- from typing import Dict, List, Union
7
-
8
- import nest_asyncio
9
- import requests
10
- from loguru import logger
11
- from tqdm import tqdm
12
-
13
- nest_asyncio.apply()
14
-
15
-
16
- def request_get(
17
- session: requests.Session,
18
- url: str,
19
- params: Dict[str, Union[int, str, float]] = None,
20
- timeout: int = 60,
21
- max_retry_times: int = 10,
22
- verbose: bool = False,
23
- ):
24
- """
25
- 單次 request,支援 retry 與 log
26
- """
27
- response = None
28
- for retry_times in range(1, max_retry_times + 1):
29
- try:
30
- response = session.get(
31
- url,
32
- verify=True,
33
- params=params,
34
- timeout=timeout,
35
- )
36
- if response.status_code == 504:
37
- if verbose:
38
- logger.warning(
39
- f"status_code=504, retry {retry_times}/{max_retry_times}"
40
- )
41
- time.sleep(retry_times * 0.1)
42
- else:
43
- break
44
- except (
45
- requests.ConnectionError,
46
- ssl.SSLError,
47
- requests.exceptions.ReadTimeout,
48
- requests.exceptions.ChunkedEncodingError,
49
- ) as exc:
50
- if verbose:
51
- logger.warning(
52
- f"Error: {exc}, retry {retry_times}/{max_retry_times}"
53
- )
54
- time.sleep(retry_times * 0.1)
55
- except Exception as exc:
56
- if verbose:
57
- logger.warning(
58
- f"Unexpected error: {exc}, retry {retry_times}/{max_retry_times}"
59
- )
60
- time.sleep(retry_times * 0.1)
61
- if response and response.status_code != 200:
62
- raise Exception(
63
- f"Final response status: {response.status_code}, text: {response.text}"
64
- )
65
- return response
66
-
67
-
68
- async def _loop_run_get(
69
- executor: concurrent.futures.ThreadPoolExecutor,
70
- loop: asyncio.AbstractEventLoop,
71
- session: requests.Session,
72
- url: str,
73
- params: Dict[str, Union[str, int, float]],
74
- timeout: int = 60,
75
- max_retry_times: int = 10,
76
- verbose: bool = False,
77
- ):
78
- """
79
- 將同步 request 包成 async
80
- """
81
- return await loop.run_in_executor(
82
- executor,
83
- request_get,
84
- session,
85
- url,
86
- params,
87
- timeout,
88
- max_retry_times,
89
- verbose,
90
- )
91
-
92
-
93
- def async_request_get(
94
- session: requests.Session,
95
- url: str,
96
- params_list: List[Dict[str, Union[str, int, float]]],
97
- timeout: int = 60,
98
- max_retry_times: int = 10,
99
- verbose: bool = False,
100
- auto_tune: bool = True,
101
- max_concurrency: int = None,
102
- batch_size: int = 10,
103
- ):
104
- """
105
- 批量 async request,支援自動根據機器資源調整 max_concurrency / batch_size
106
- """
107
- # 自動調整
108
- if auto_tune:
109
- cpu_count = os.cpu_count() or 1
110
- max_concurrency = max_concurrency or max(1, cpu_count * 2)
111
- else:
112
- max_concurrency = max_concurrency or 10
113
-
114
- async def runner():
115
- semaphore = asyncio.Semaphore(max_concurrency)
116
- loop = asyncio.get_event_loop()
117
- executor = concurrent.futures.ThreadPoolExecutor(
118
- max_workers=max_concurrency
119
- )
120
-
121
- async def _limited_run(params):
122
- async with semaphore:
123
- resp = await _loop_run_get(
124
- executor,
125
- loop,
126
- session,
127
- url=url,
128
- params=params,
129
- timeout=timeout,
130
- max_retry_times=max_retry_times,
131
- verbose=verbose,
132
- )
133
- return resp
134
-
135
- results = []
136
- pbar = tqdm(total=len(params_list))
137
-
138
- # 分 batch 建立 task
139
- for i in range(0, len(params_list), batch_size):
140
- batch = params_list[i : i + batch_size]
141
- tasks = [asyncio.create_task(_limited_run(p)) for p in batch]
142
-
143
- for coro in asyncio.as_completed(tasks):
144
- try:
145
- res = await coro
146
- results.append(res)
147
- except Exception as exc:
148
- if verbose:
149
- logger.error(f"Task failed: {exc}")
150
- pbar.update(1)
151
-
152
- pbar.close()
153
- executor.shutdown()
154
- return results
155
-
156
- return asyncio.run(runner())
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