neurostats-API 0.0.13__py3-none-any.whl → 0.0.15__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1 +1,13 @@
1
- __version__='0.0.13'
1
+ __version__='0.0.15'
2
+
3
+ from .fetchers import (
4
+ BalanceSheetFetcher,
5
+ CashFlowFetcher,
6
+ FinanceOverviewFetcher,
7
+ FinanceReportFetcher,
8
+ InstitutionFetcher,
9
+ MarginTradingFetcher,
10
+ MonthRevenueFetcher,
11
+ TechFetcher,
12
+ ProfitLoseFetcher
13
+ )
@@ -2,6 +2,8 @@ from .base import StatsDateTime, StatsFetcher
2
2
  from .balance_sheet import BalanceSheetFetcher
3
3
  from .cash_flow import CashFlowFetcher
4
4
  from .finance_overview import FinanceOverviewFetcher
5
+ from .tej_finance_report import FinanceReportFetcher
6
+ from .tech import TechFetcher
5
7
  from .institution import InstitutionFetcher
6
8
  from .margin_trading import MarginTradingFetcher
7
9
  from .month_revenue import MonthRevenueFetcher
@@ -1,16 +1,16 @@
1
+ import abc
1
2
  from pymongo import MongoClient
2
3
  import pandas as pd
3
4
  import json
4
5
  import pytz
5
6
  from datetime import datetime, timedelta, date
6
- from ..utils import StatsDateTime, StatsProcessor
7
+ from ..utils import StatsDateTime, StatsProcessor, YoY_Calculator
7
8
  import yaml
8
9
 
9
10
  class StatsFetcher:
10
11
  def __init__(self, ticker, db_client):
11
12
  self.ticker = ticker
12
- self.db = db_client[
13
- "company"] # Replace with your database name
13
+ self.db = db_client["company"] # Replace with your database name
14
14
  self.collection = self.db["twse_stats"]
15
15
 
16
16
  self.timezone = pytz.timezone("Asia/Taipei")
@@ -37,7 +37,7 @@ class StatsFetcher:
37
37
  ]
38
38
 
39
39
  def collect_data(self, start_date, end_date):
40
- pipeline = self.prepare_query(start_date, end_date)
40
+ pipeline = self.prepare_query()
41
41
 
42
42
  fetched_data = list(self.collection.aggregate(pipeline))
43
43
 
@@ -52,3 +52,101 @@ class StatsFetcher:
52
52
  season = (month - 1) // 3 + 1
53
53
 
54
54
  return StatsDateTime(date, year, month, day, season)
55
+
56
+ class BaseTEJFetcher(abc.ABC):
57
+ def __init__(self):
58
+ self.client = None
59
+ self.db = None
60
+ self.collection = None
61
+
62
+ @abc.abstractmethod
63
+ def get(self):
64
+ pass
65
+
66
+ def get_latest_data_time(self, ticker):
67
+ latest_data = self.collection.find_one(
68
+ {"ticker": ticker},
69
+ {"last_update": 1, "_id" : 0}
70
+ )
71
+
72
+ try:
73
+ latest_date = latest_data['last_update']["latest_data_date"]
74
+ except Exception as e:
75
+ latest_date = None
76
+
77
+ return latest_date
78
+
79
+ def cal_YoY(self, data_dict: dict, start_year: int, end_year: int):
80
+ year_shifts = [1,3,5,10]
81
+ return_dict = {}
82
+ for year in range(start_year, end_year+1):
83
+ year_data = data_dict[str(year)]
84
+ year_keys = list(year_data.keys())
85
+ for key in year_keys:
86
+ if (key in 'season'):
87
+ continue
88
+
89
+ if (isinstance(year_data[key], (int, float))):
90
+ temp_dict = {"value": year_data[key]}
91
+
92
+ for shift in year_shifts:
93
+ this_value = year_data[key]
94
+ try:
95
+ past_year = str(year - shift)
96
+ last_value = data_dict[past_year][key]['value']
97
+ temp_dict[f"YoY_{shift}"] = YoY_Calculator.cal_growth(
98
+ this_value, last_value, delta = shift
99
+ )
100
+ except Exception as e:
101
+ temp_dict[f"YoY_{shift}"] = None
102
+
103
+ year_data[key] = temp_dict
104
+
105
+ else:
106
+ year_data.pop(key)
107
+
108
+ return_dict[year] = year_data
109
+
110
+
111
+ return return_dict
112
+
113
+ def cal_QoQ(self, data_dict):
114
+ return_dict = {}
115
+ for i, time_index in enumerate(data_dict.keys()):
116
+ year, season = time_index.split("Q")
117
+ year = int(year)
118
+ season = int(season)
119
+ if (season == 1):
120
+ last_year = year - 1
121
+ last_season = 4
122
+ else:
123
+ last_year = year
124
+ last_season = season - 1
125
+
126
+ this_data = data_dict[time_index]
127
+ this_keys = list(this_data.keys())
128
+ for key in this_keys:
129
+ if (key in 'season'):
130
+ continue
131
+
132
+ this_value = this_data[key]
133
+
134
+ if (isinstance(this_value, (int, float))):
135
+ temp_dict = {"value": this_value}
136
+
137
+ try:
138
+ last_value = data_dict[f"{last_year}Q{last_season}"][key]['value']
139
+
140
+ temp_dict['growth'] = YoY_Calculator.cal_growth(
141
+ this_value, last_value, delta=1
142
+ )
143
+ except Exception as e:
144
+ temp_dict['growth'] = None
145
+
146
+ this_data[key] = temp_dict
147
+
148
+ else:
149
+ this_data.pop(key)
150
+ return_dict[time_index] = this_data
151
+ return return_dict
152
+
@@ -164,25 +164,26 @@ class InstitutionFetcher(StatsFetcher):
164
164
  latest_daily_data['volume'])
165
165
  }
166
166
  # 一年內法人
167
- annual_dates = [data['date'].strftime("%Y-%m-%d") for data in daily_datas]
167
+ annual_dates = [
168
+ data['date'].strftime("%Y-%m-%d") for data in daily_datas
169
+ ]
168
170
  annual_closes = {
169
- data['date'].strftime("%Y-%m-%d") : data['close']
171
+ data['date'].strftime("%Y-%m-%d"): data['close']
170
172
  for data in daily_datas
171
173
  if (data['date'].strftime("%Y-%m-%d") in annual_dates)
172
174
  }
173
175
  annual_volumes = {
174
- data['date'].strftime("%Y-%m-%d") : data['volume']
176
+ data['date'].strftime("%Y-%m-%d"): data['volume']
175
177
  for data in daily_datas
176
178
  if (data['date'].strftime("%Y-%m-%d") in annual_dates)
177
179
  }
178
180
  annual_trading = {
179
- data['date'].strftime("%Y-%m-%d") : data
181
+ data['date'].strftime("%Y-%m-%d"): data
180
182
  for data in institution_tradings
181
- }
183
+ }
182
184
 
183
185
  annual_trading = {
184
186
  date: {
185
-
186
187
  "close": annual_closes[date],
187
188
  "volume": annual_volumes[date],
188
189
  **annual_trading[date]
@@ -217,13 +218,18 @@ class InstitutionFetcher(StatsFetcher):
217
218
  self.target_institution(latest_trading,
218
219
  latest_table['institutional_investor'],
219
220
  key, volume)
220
-
221
- for trade_type in ['buy', 'sell']:
222
- for unit in ['stock', 'percentage']:
223
- latest_table['institutional_investor'][trade_type][
224
- unit] = (latest_table['foreign'][trade_type][unit] +
225
- latest_table['prop'][trade_type][unit] +
226
- latest_table['mutual'][trade_type][unit])
221
+ # 計算合計
222
+ for unit in ['stock', 'percentage']:
223
+ # 買進總和
224
+ latest_table['institutional_investor']['buy'][unit] = (
225
+ latest_table['foreign']['buy'][unit] +
226
+ latest_table['prop']['buy'][unit] +
227
+ latest_table['mutual']['buy'][unit])
228
+ # 賣出總和
229
+ latest_table['institutional_investor']['sell'][unit] = (
230
+ latest_table['foreign']['sell'][unit] +
231
+ latest_table['prop']['sell'][unit] +
232
+ latest_table['mutual']['sell'][unit])
227
233
 
228
234
  frames = []
229
235
  for category, trades in latest_table.items():
@@ -21,12 +21,6 @@ class ProfitLoseFetcher(StatsFetcher):
21
21
  def prepare_query(self, target_season):
22
22
  pipeline = super().prepare_query()
23
23
 
24
- target_query = {
25
- "year": "$$target_season_data.year",
26
- "season": "$$target_season_data.season",
27
- "balance_sheet": "$$$$target_season_data.balance_sheet"
28
- }
29
-
30
24
  pipeline.append({
31
25
  "$project": {
32
26
  "_id": 0,
@@ -121,6 +115,9 @@ class ProfitLoseFetcher(StatsFetcher):
121
115
  elif ('YoY' in item_name):
122
116
  if (isinstance(item, (float, int))):
123
117
  item = StatsProcessor.cal_percentage(item)
118
+ elif ('每股盈餘' in index_name):
119
+ if (isinstance(item, (float, int))):
120
+ item = StatsProcessor.cal_non_percentage(item, postfix="元")
124
121
  else:
125
122
  if (isinstance(item, (float, int))):
126
123
  item = StatsProcessor.cal_non_percentage(item, postfix="千元")
@@ -153,6 +150,9 @@ class ProfitLoseFetcher(StatsFetcher):
153
150
  target_index=target_index)
154
151
  break
155
152
  except Exception as e:
156
- continue
153
+ return_dict[name] = StatsProcessor.slice_multi_col_table(
154
+ total_table=total_table,
155
+ mode=setting['mode'],
156
+ target_index=target_index)
157
157
 
158
158
  return return_dict
@@ -1,9 +1,16 @@
1
1
  from .base import StatsFetcher
2
2
  import pandas as pd
3
+ import yfinance as yf
3
4
 
4
5
  class TechFetcher(StatsFetcher):
5
6
 
6
7
  def __init__(self, ticker:str, db_client):
8
+
9
+ """
10
+ The Capitalization-Weighted Index includes the following tickers:
11
+ ['GSPC', 'IXIC', 'DJI', 'TWII']
12
+ """
13
+
7
14
  super().__init__(ticker, db_client)
8
15
  self.full_ohlcv = self._get_ohlcv()
9
16
  self.basic_indexes = ['SMA5', 'SMA20', 'SMA60', 'EMA5', 'EMA20',
@@ -40,16 +47,36 @@ class TechFetcher(StatsFetcher):
40
47
  )
41
48
 
42
49
  def _get_ohlcv(self):
43
- query = {'ticker': self.ticker}
44
- ticker_full = list(self.collection.find(query))
50
+
51
+ if self.ticker in ['GSPC', 'IXIC', 'DJI', 'TWII']:
52
+
53
+ full_tick = f'^{self.ticker}'
54
+ yf_ticker = yf.Ticker(full_tick)
55
+ origin_df = yf_ticker.history(period="10y")
56
+ origin_df = origin_df.reset_index()
57
+ origin_df["Date"] = pd.to_datetime(origin_df["Date"]).dt.date
58
+ df = origin_df.rename(
59
+ columns={
60
+ "Date": "date",
61
+ "Open": "open",
62
+ "High": "high",
63
+ "Low": "low",
64
+ "Close": "close",
65
+ "Volume": "volume"
66
+ }
67
+ )
68
+ else:
69
+
70
+ query = {'ticker': self.ticker}
71
+ ticker_full = list(self.collection.find(query))
45
72
 
46
- if not ticker_full:
47
- raise ValueError(f"No data found for ticker: {self.ticker}")
73
+ if not ticker_full:
74
+ raise ValueError(f"No data found for ticker: {self.ticker}")
48
75
 
49
- if 'daily_data' not in ticker_full[0] or ticker_full[0]['daily_data'] is None:
50
- raise KeyError("Missing 'daily_data' in the retrieved data")
76
+ if 'daily_data' not in ticker_full[0] or ticker_full[0]['daily_data'] is None:
77
+ raise KeyError("Missing 'daily_data' in the retrieved data")
51
78
 
52
- df = pd.DataFrame(ticker_full[0]['daily_data'])
79
+ df = pd.DataFrame(ticker_full[0]['daily_data'])
53
80
 
54
81
  selected_cols = ['date','open','high','low','close','volume']
55
82
 
@@ -0,0 +1,313 @@
1
+ from .base import BaseTEJFetcher
2
+ from datetime import datetime
3
+ from enum import Enum
4
+ import pandas as pd
5
+ from pymongo import MongoClient
6
+ from ..utils import StatsProcessor, YoY_Calculator
7
+ import warnings
8
+
9
+
10
+ class FinanceReportFetcher(BaseTEJFetcher):
11
+ class FetchMode(Enum):
12
+ YOY = 1
13
+ QOQ = 2
14
+ YOY_NOCAL = 3
15
+ QOQ_NOCAL = 4
16
+
17
+ def __init__(self, mongo_uri, db_name = "company", collection_name = "TWN/AINVFQ1"):
18
+ self.client = MongoClient(mongo_uri)
19
+ self.db = self.client[db_name]
20
+ self.collection = self.db[collection_name]
21
+
22
+ self.check_index = {
23
+ 'coid', 'mdate', 'key3', 'no',
24
+ 'sem', 'merg', 'curr', 'annd',
25
+ 'fin_ind', 'bp11', 'bp21', 'bp22',
26
+ 'bp31', 'bp41', 'bp51', 'bp53',
27
+ 'bp61', 'bp62', 'bp63', 'bp64',
28
+ 'bp65', 'bf11', 'bf12', 'bf21',
29
+ 'bf22', 'bf41', 'bf42', 'bf43',
30
+ 'bf44', 'bf45', 'bf99', 'bsca',
31
+ 'bsnca', 'bsta','bscl','bsncl',
32
+ 'bstl','bsse','bslse','debt',
33
+ 'quick','ppe','ar','ip12',
34
+ 'ip22','ip31','ip51','iv41',
35
+ 'if11','isibt','isni','isnip',
36
+ 'eps','ispsd','gm','opi',
37
+ 'nri','ri','nopi','ebit',
38
+ 'cip31','cscfo','cscfi','cscff',
39
+ 'person','shares','wavg','taxrate',
40
+ 'r104','r115','r105','r106',
41
+ 'r107','r108','r201','r112',
42
+ 'r401','r402','r403','r404',
43
+ 'r405','r408','r409','r410',
44
+ 'r502','r501','r205','r505',
45
+ 'r517','r512','r509','r608',
46
+ 'r616','r610','r607','r613',
47
+ 'r612','r609','r614','r611',
48
+ 'r307','r304','r305','r306',
49
+ 'r316','r834'
50
+ }
51
+
52
+ def get(
53
+ self,
54
+ ticker,
55
+ fetch_mode: FetchMode = FetchMode.QOQ,
56
+ start_date: str = None,
57
+ end_date: str = None,
58
+ report_type: str = "Q",
59
+ indexes: list = []
60
+ ):
61
+ """
62
+ 基礎的query function
63
+ ticker(str): 股票代碼
64
+ start_date(str): 開頭日期範圍
65
+ end_date(str): = 結束日期範圍
66
+ report_type(str): 報告型態 {"A", "Q", "TTM"}
67
+ fetch_mode(class FetchMode):
68
+ YoY : 起始日期到結束日期範圍內,特定該季的資料
69
+ QoQ : 起始日期到結束日期內,每季的資料(與上一季成長率)
70
+ indexes(List): 指定的index
71
+ """
72
+ # 確認indexes中是否有錯誤的index,有的話回傳warning
73
+ if (indexes and self.check_index):
74
+ indexes = set(indexes)
75
+ difference = indexes-self.check_index
76
+ if (difference):
77
+ warnings.warn(
78
+ f"{list(difference)} 沒有出現在資料表中,請確認column名稱是否正確",
79
+ UserWarning
80
+ )
81
+
82
+
83
+ if (fetch_mode in {
84
+ self.FetchMode.QOQ,
85
+ self.FetchMode.QOQ_NOCAL
86
+ }
87
+ ):
88
+ if (not start_date):
89
+ warnings.warn("No start_date specified, use default date = \"2005-01-01\"", UserWarning)
90
+ start_date = datetime.strptime("2005-01-01", "%Y-%m-%d")
91
+ if (not end_date):
92
+ warnings.warn("No end_date specified, use default date = today", UserWarning)
93
+ end_date = datetime.today()
94
+
95
+ assert (start_date <= end_date)
96
+ start_date = datetime.strptime(start_date, "%Y-%m-%d")
97
+ end_date = datetime.strptime(end_date, "%Y-%m-%d")
98
+
99
+ start_year = start_date.year
100
+ start_season = (start_date.month - 1) // 4 + 1
101
+ end_year = end_date.year
102
+ end_season = (end_date.month - 1) // 4 + 1
103
+
104
+ if (fetch_mode == self.FetchMode.QOQ):
105
+ use_cal = True
106
+ else:
107
+ use_cal = False
108
+
109
+ data_df = self.get_QoQ_data(
110
+ ticker=ticker,
111
+ start_year=start_year,
112
+ start_season=start_season,
113
+ end_year=end_year,
114
+ end_season=end_season,
115
+ report_type=report_type,
116
+ indexes=indexes,
117
+ use_cal=use_cal
118
+ )
119
+
120
+ return data_df
121
+
122
+ elif (fetch_mode in {
123
+ self.FetchMode.YOY,
124
+ self.FetchMode.YOY_NOCAL
125
+ }
126
+ ):
127
+ start_year = 2005
128
+ end_date = self.get_latest_data_time(ticker)
129
+ if (not end_date):
130
+ end_date = datetime.today()
131
+
132
+ end_year = end_date.year
133
+ season = (end_date.month - 1) // 4 + 1
134
+
135
+ if (fetch_mode == self.FetchMode.YOY):
136
+ use_cal = True
137
+ else:
138
+ use_cal = False
139
+
140
+ data_df = self.get_YoY_data(
141
+ ticker = ticker,
142
+ start_year = start_year,
143
+ end_year = end_year,
144
+ season = season,
145
+ report_type = report_type,
146
+ indexes = indexes
147
+ )
148
+
149
+ return data_df
150
+
151
+ def get_QoQ_data(
152
+ self,
153
+ ticker,
154
+ start_year,
155
+ start_season,
156
+ end_year,
157
+ end_season,
158
+ report_type = "Q",
159
+ indexes = [],
160
+ use_cal = False
161
+ ):
162
+ """
163
+ 取得時間範圍內每季資料
164
+ """
165
+ if (not indexes): # 沒有指定 -> 取全部
166
+ pipeline = [
167
+ { "$match": { "ticker": ticker } },
168
+ { "$unwind": "$data" },
169
+ { "$match": {
170
+ "$or": [
171
+ { "data.year": { "$gt": start_year, "$lt": end_year } },
172
+ { "data.year": start_year, "data.season": { "$gte": start_season } },
173
+ { "data.year": end_year, "data.season": { "$lte": end_season } }
174
+ ]
175
+ }},
176
+ { "$project": {
177
+ "data.year": 1,
178
+ "data.season": 1,
179
+ f"data.{report_type}": 1,
180
+ "_id": 0
181
+ }
182
+ }
183
+ ]
184
+
185
+
186
+ else: # 取指定index
187
+ project_stage = {
188
+ "data.year": 1,
189
+ "data.season": 1
190
+ }
191
+ for index in indexes:
192
+ project_stage[f"data.{report_type}.{index}"] = 1
193
+
194
+ pipeline = [
195
+ { "$match": { "ticker": ticker } },
196
+ { "$unwind": "$data" },
197
+ { "$match": {
198
+ "$or": [
199
+ { "data.year": { "$gt": start_year, "$lt": end_year } },
200
+ { "data.year": start_year, "data.season": { "$gte": start_season } },
201
+ { "data.year": end_year, "data.season": { "$lte": end_season } }
202
+ ]
203
+ }},
204
+ { "$project": project_stage }
205
+ ]
206
+
207
+
208
+ fetched_data = self.collection.aggregate(pipeline).to_list()
209
+
210
+ data_dict = StatsProcessor.list_of_dict_to_dict(
211
+ fetched_data,
212
+ keys = ["year", "season"],
213
+ delimeter = "Q",
214
+ data_key=report_type
215
+ )
216
+ if (use_cal):
217
+ data_with_QoQ = self.cal_QoQ(data_dict)
218
+ data_df = pd.DataFrame.from_dict(data_with_QoQ)
219
+ else:
220
+ data_df = pd.DataFrame.from_dict(data_dict)
221
+ return data_df
222
+
223
+ def get_YoY_data(
224
+ self,
225
+ ticker,
226
+ start_year,
227
+ end_year,
228
+ season,
229
+ report_type = "Q",
230
+ indexes = [],
231
+ use_cal = False
232
+ ):
233
+ """
234
+ 取得某季歷年資料
235
+ """
236
+ if (use_cal):
237
+ select_year = set()
238
+
239
+ for year in range(start_year, end_year + 1):
240
+ year_shifts = {
241
+ year,
242
+ year - 1,
243
+ year - 3,
244
+ year - 5,
245
+ year - 10
246
+ }
247
+
248
+ select_year = select_year.union(year_shifts)
249
+
250
+ select_year = sorted(list(select_year), reverse=True)
251
+ else:
252
+ select_year = [year for year in range(start_year, end_year + 1)]
253
+
254
+ if (not indexes): # 沒有指定 -> 取全部
255
+ pipeline = [
256
+ { "$match": { "ticker": ticker } },
257
+ { "$unwind": "$data" },
258
+ { "$match": {
259
+ "$or":[
260
+ {
261
+ "$and": [
262
+ { "data.year": {"$in": select_year }},
263
+ { "data.season": {"$eq": season}}
264
+ ]
265
+ },
266
+ ]
267
+ }},
268
+ { "$project": {
269
+ "data.year": 1,
270
+ "data.season": 1,
271
+ f"data.{report_type}": 1,
272
+ "_id": 0
273
+ }
274
+ }
275
+ ]
276
+
277
+
278
+ else: # 取指定index
279
+ project_stage = {
280
+ "data.year": 1,
281
+ "data.season": 1
282
+ }
283
+ for index in indexes:
284
+ project_stage[f"data.{report_type}.{index}"] = 1
285
+
286
+ pipeline = [
287
+ { "$match": { "ticker": ticker } },
288
+ { "$unwind": "$data" },
289
+ { "$match": {
290
+ "$and": [
291
+ { "data.year": {"$in": select_year}},
292
+ { "data.season": {"$eq": season}}
293
+ ]
294
+ }},
295
+ { "$project": project_stage }
296
+ ]
297
+
298
+ fetched_data = self.collection.aggregate(pipeline).to_list()
299
+
300
+ # 處理計算YoY
301
+ data_dict = StatsProcessor.list_of_dict_to_dict(
302
+ fetched_data,
303
+ keys = ['year', 'season'],
304
+ data_key=report_type,
305
+ delimeter='Q'
306
+ )
307
+ if (use_cal):
308
+ data_with_YoY = self.cal_YoY(data_dict, start_year, end_year)
309
+ result_df = pd.DataFrame.from_dict(data_with_YoY)
310
+ else:
311
+ result_df = pd.DataFrame.from_dict(data_dict)
312
+
313
+ return result_df
@@ -21,6 +21,7 @@ current_debt:
21
21
  mode: value_and_percentage
22
22
  target_index:
23
23
  - 流動負債合計
24
+ - 流動負債總額
24
25
 
25
26
  non_current_debt:
26
27
  mode: value_and_percentage
@@ -3,38 +3,45 @@ profit_lose: # 總營收表
3
3
 
4
4
  grand_total_profit_lose:
5
5
  mode: grand_total_values
6
-
6
+
7
7
  revenue:
8
8
  mode: growth
9
9
  target_index:
10
10
  - 營業收入合計
11
+ - 收入合計
11
12
  - 利息收入
12
13
 
13
14
  grand_total_revenue:
14
15
  mode: grand_total_growth
15
16
  target_index:
16
17
  - 營業收入合計
18
+ - 收入合計
17
19
  - 利息收入
18
20
 
19
21
  gross_profit:
20
22
  mode: growth
21
23
  target_index:
22
24
  - 營業毛利(毛損)淨額
25
+ - 利息淨收益
23
26
 
24
27
  grand_total_gross_profit:
25
28
  mode: grand_total_growth
26
29
  target_index:
27
30
  - 營業毛利(毛損)淨額
28
-
31
+ - 利息淨收益
32
+
29
33
  gross_profit_percentage:
30
34
  mode: percentage
31
35
  target_index:
32
36
  - 營業毛利(毛損)淨額
37
+ - 利息淨收益
33
38
 
34
39
  grand_total_gross_profit_percentage:
35
40
  mode: grand_total_percentage
36
41
  target_index:
37
42
  - 營業毛利(毛損)淨額
43
+ - 利息淨收益
44
+
38
45
  # 營利
39
46
  operating_income:
40
47
  mode: growth
@@ -55,46 +62,61 @@ grand_total_operating_income_percentage:
55
62
  mode: grand_total_percentage
56
63
  target_index:
57
64
  - 營業利益(損失)
65
+
58
66
  # 稅前淨利
59
67
  net_income_before_tax:
60
68
  mode: growth
61
69
  target_index:
62
70
  - 稅前淨利(淨損)
71
+ - 繼續營業單位稅前損益
72
+ - 繼續營業單位稅前淨利(淨損)
63
73
 
64
74
  grand_total_net_income_before_tax:
65
75
  mode: grand_total_growth
66
76
  target_index:
67
77
  - 稅前淨利(淨損)
78
+ - 繼續營業單位稅前損益
79
+ - 繼續營業單位稅前淨利(淨損)
68
80
 
69
81
  net_income_before_tax_percentage:
70
82
  mode: percentage
71
83
  target_index:
72
84
  - 稅前淨利(淨損)
85
+ - 繼續營業單位稅前損益
86
+ - 繼續營業單位稅前淨利(淨損)
73
87
 
74
88
  grand_total_net_income_before_tax_percentage:
75
89
  mode: grand_total_percentage
76
90
  target_index:
77
91
  - 稅前淨利(淨損)
92
+ - 繼續營業單位稅前損益
93
+ - 繼續營業單位稅前淨利(淨損)
94
+
78
95
  # 本期淨利
79
96
  net_income:
80
97
  mode: growth
81
98
  target_index:
82
99
  - 本期淨利(淨損)
100
+ - 本期稅後淨利(淨損)
83
101
 
84
102
  grand_total_net_income:
85
103
  mode: grand_total_growth
86
104
  target_index:
87
105
  - 本期淨利(淨損)
106
+ - 本期稅後淨利(淨損)
88
107
 
89
108
  net_income_percentage:
90
109
  mode: percentage
91
110
  target_index:
92
111
  - 本期淨利(淨損)
112
+ - 本期稅後淨利(淨損)
93
113
 
94
114
  grand_total_income_percentage:
95
115
  mode: grand_total_percentage
96
116
  target_index:
97
117
  - 本期淨利(淨損)
118
+ - 本期稅後淨利(淨損)
119
+
98
120
  # EPS
99
121
  EPS:
100
122
  mode: value
@@ -118,4 +140,4 @@ grand_total_EPS_growth:
118
140
  mode: grand_total_growth
119
141
  target_index:
120
142
  - 基本每股盈餘
121
- - 基本每股盈餘合計
143
+ - 基本每股盈餘合計
@@ -1,4 +1,5 @@
1
1
  from .datetime import StatsDateTime
2
2
  from .db_client import DBClient
3
3
  from .data_process import StatsProcessor
4
- from .fetcher import StatsFetcher
4
+ from .fetcher import StatsFetcher
5
+ from .calculate_value import YoY_Calculator
@@ -0,0 +1,26 @@
1
+ class YoY_Calculator:
2
+ def __init__(self):
3
+ pass
4
+
5
+ @classmethod
6
+ def cal_growth(cls, target_value: float, past_value: float, delta: int):
7
+ """
8
+ 計算成長率以及年化成長率
9
+ target_value: float,這個時間的數值
10
+ past_value: float,過去的這個時間數值
11
+ delta: int,代表隔了幾年/季 delta > 1 時改以年化成長率計算
12
+ """
13
+ try:
14
+ if (delta > 1):
15
+ YoY = ((target_value / past_value)**(1 / delta)) - 1
16
+
17
+ else:
18
+ YoY = ((target_value - past_value) / past_value)
19
+
20
+ except Exception as e:
21
+ return None
22
+
23
+ if (isinstance(YoY, complex)): # 年化成長率有複數問題
24
+ return None
25
+
26
+ return YoY
@@ -20,7 +20,6 @@ class StatsProcessor:
20
20
  """
21
21
  1. 讀檔: txt / yaml
22
22
  2. 將巢狀dictionary / DataFrame扁平化
23
-
24
23
  """
25
24
 
26
25
  @classmethod
@@ -97,20 +96,47 @@ class StatsProcessor:
97
96
  """
98
97
  對Multicolumn的dataframe切出目標的index
99
98
  """
100
- times = total_table.columns.get_level_values(0).unique()
101
99
  try:
102
100
  target_metrics = target_metric_dict[mode]
103
101
  except KeyError as e:
104
102
  return f"mode Error: Get mode should be {list(target_metric_dict.keys())} but get {mode}"
105
103
 
104
+ times = total_table.columns.get_level_values(0).unique()
106
105
  desired_order = [(time, value_name) for time in times
107
106
  for value_name in target_metrics]
108
107
 
109
108
  if (target_index):
110
109
  target_index = target_index.split()
111
- sliced_table = total_table.loc[
112
- target_index, pd.IndexSlice[:,
113
- target_metrics]][desired_order].T
110
+ try:
111
+ sliced_table = total_table.loc[
112
+ target_index,
113
+ pd.IndexSlice[:, target_metrics]][desired_order].T
114
+
115
+ except Exception as e: # 沒辦法完整取得表格
116
+ # 先設立空表格
117
+ empty_index = pd.Index(desired_order)
118
+ empty_columns = pd.Index(target_index)
119
+ sliced_table = pd.DataFrame(index=empty_index,
120
+ columns=empty_columns)
121
+
122
+ try:
123
+ # 提取有效的部分資料
124
+ partial_table = total_table.loc[
125
+ total_table.index.intersection(target_index),
126
+ pd.IndexSlice[:, target_metrics]
127
+ ]
128
+
129
+ # 遍歷 partial_table 的索引和值,手動填入 sliced_table
130
+ for row_index in partial_table.index:
131
+ for col_index in partial_table.columns:
132
+ if col_index in desired_order and row_index in target_index:
133
+ sliced_table.loc[col_index, row_index] = partial_table.loc[row_index, col_index]
134
+
135
+ # 確保 `sliced_table` 的排序符合 `desired_order`
136
+ sliced_table = sliced_table.reindex(index=desired_order, columns=target_index)
137
+ except Exception as sub_e:
138
+ pass
139
+
114
140
  if (mode == 'value_and_percentage'): # 因應balance_sheet 頁面的格式
115
141
  return_table = sliced_table.T
116
142
  return_table.columns = [
@@ -146,7 +172,7 @@ class StatsProcessor:
146
172
  @classmethod
147
173
  def cal_percentage(cls, value, postfix="%"):
148
174
  if (isinstance(value, (float, int))):
149
- value = np.round(value * 100 , 2).item()
175
+ value = np.round(value * 100, 2).item()
150
176
  value = f"{value:.2f}{postfix}"
151
177
 
152
178
  return value
@@ -157,7 +183,7 @@ class StatsProcessor:
157
183
  @classmethod
158
184
  def cal_non_percentage(cls, value, to_str=False, postfix="元"):
159
185
  if (isinstance(value, (float, int))):
160
-
186
+
161
187
  value = np.round(value, 2).item()
162
188
  if (postfix == "千元"):
163
189
  value *= 1000
@@ -178,7 +204,7 @@ class StatsProcessor:
178
204
 
179
205
  else:
180
206
  return value
181
-
207
+
182
208
  @classmethod
183
209
  def cal_round_int(cls, value):
184
210
  """
@@ -188,3 +214,59 @@ class StatsProcessor:
188
214
  return int(np.round(value).item())
189
215
  else:
190
216
  return value
217
+
218
+ @classmethod
219
+ def list_of_dict_to_dict(
220
+ cls,
221
+ data_list: list,
222
+ key: str = "",
223
+ keys: list = [],
224
+ delimeter: str = "_",
225
+ data_key: str = "Q"
226
+ ):
227
+ """
228
+ TEJ DB 用
229
+ List[Dict] -> Dict[Dict]
230
+ input:
231
+ data_list(List):
232
+ [
233
+ { "data":
234
+ {
235
+ "year": 2021...
236
+ "season": 1,
237
+ "Q": {}...
238
+
239
+ }
240
+ }
241
+ ]
242
+
243
+ key(str): 選擇哪一個key作為轉化後的index
244
+ delimeter(str): 多個key時要用甚麼分隔
245
+ return:
246
+ {
247
+ "2021" : {# Q下的資料} ...
248
+ }
249
+
250
+ or (keys = ['year', 'season'])
251
+ {
252
+ "2021Q2" : {}
253
+ }
254
+ """
255
+ assert (key or keys), "func list_of_dict_to_dict must have argument \"key\" or \"keys\""
256
+
257
+ return_dict = {}
258
+ if (key):
259
+ keys = [key]
260
+ for data in data_list:
261
+ data = data['data']
262
+
263
+ pop_keys = []
264
+
265
+ for key in keys:
266
+ assert (key in data.keys())
267
+ pop_keys.append(str(data.pop(key)))
268
+
269
+ pop_key = delimeter.join(pop_keys)
270
+ return_dict[pop_key] = data[data_key]
271
+
272
+ return return_dict
@@ -1,12 +1,18 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: neurostats-API
3
- Version: 0.0.13
3
+ Version: 0.0.15
4
4
  Summary: The service of NeuroStats website
5
5
  Home-page: https://github.com/NeurowattStats/NeuroStats_API.git
6
6
  Author: JasonWang@Neurowatt
7
7
  Author-email: jason@neurowatt.ai
8
8
  Requires-Python: >=3.6
9
9
  Description-Content-Type: text/markdown
10
+ Requires-Dist: numpy>=2.1.0
11
+ Requires-Dist: pandas>=2.2.0
12
+ Requires-Dist: pymongo
13
+ Requires-Dist: pytz
14
+ Requires-Dist: python-dotenv
15
+ Requires-Dist: yfinance
10
16
 
11
17
  # neurostats_API
12
18
 
@@ -83,7 +89,7 @@ pip install neurostats-API
83
89
  ```Python
84
90
  >>> import neurostats_API
85
91
  >>> print(neurostats_API.__version__)
86
- 0.0.13
92
+ 0.0.15
87
93
  ```
88
94
 
89
95
  ### 得到最新一期的評價資料與歷年評價
@@ -667,7 +673,71 @@ fetcher.query()
667
673
 
668
674
  請注意`range`, `last_range`, `52week_range`這三個項目型態為字串,其餘為float
669
675
 
676
+
677
+ ## TEJ 相關
678
+ ### 會計師簽證財務資料
679
+ ```Python
680
+ from neurostats_API import FinanceReportFetcher
681
+
682
+ mongo_uri = <MongoDB 的 URI>
683
+ db_name = 'company' # 連接的DB名稱
684
+ collection_name = "TWN/AINVFQ1" # 連接的collection對象
685
+
686
+ fetcher = FinanceReportFetcher(
687
+ mongo_uri = mongo_uri,
688
+ db_name = db_name,
689
+ collection_name = collection_name
690
+ )
691
+
692
+ data = fetcher.get(
693
+ ticker = "2330" # 任意的股票代碼
694
+ fetch_mode = fetcher.YOY_NOCAL # 取得模式
695
+ start_date = "2005-01-01",
696
+ end_date = "2024-12-31",
697
+ report_type = "Q",
698
+ indexes = []
699
+ )
700
+ ```
701
+ - `ticker`: 股票代碼
702
+
703
+ - `fetch_mode` : 取得模式,為`fetcher.YOY_NOCAL` 或 `fetcher.QOQ_NOCAL`
704
+ - `YOY_NOCAL`: 以end_date為準,取得與end_date為準同季的歷年資料,時間範圍以start_date為準
705
+ > 例如`start_date = "2020-07-01"`, `end_date = "2024-01-01"`,會回傳2020~2024的第一季資料
706
+
707
+ - `QOQ_NOCAL`: 時間範圍內的每季資料
708
+
709
+ - `start_date`: 開始日期,不設定時預設為`2005-01-01`
710
+
711
+ - `end_date`: 結束日期,不設定時預設為資料庫最新資料的日期
712
+
713
+ - `report_type`: 選擇哪種報告,預設為`Q`
714
+ - `A`: 當年累計
715
+ - `Q`: 當季數值
716
+ - `TTM`: 移動四季 (包括當季在內,往前累計四個季度)
717
+
718
+ - `indexes`: 選擇的column,需要以TEJ提供的欄位名稱為準,不提供時或提供`[]`會回傳全部column
719
+ - 範例輸入: `['bp41', 'bp51']`
720
+
721
+ [TEJ資料集連結](https://tquant.tejwin.com/%E8%B3%87%E6%96%99%E9%9B%86/)
722
+ 請看 `會計師簽證財務資料`
723
+
724
+ #### 回傳資料
725
+ fetch_mode設定為`YOY_NOCAL`與`QOQ_NOCAL`下
726
+ 為回傳pd.DataFrame,column名稱為<年份>Q<季>, row名稱為指定財報項目
727
+
670
728
  ## 版本紀錄
729
+ ## 0.0.15
730
+ - TechFetcher中新增指數條件
731
+
732
+ - 新增tej_fetcher索取TEJ相關的資料
733
+
734
+ - package新增depensnecy,可以安裝需要的相關package
735
+
736
+ ## 0.0.14
737
+ - 修改部分財報資料錯誤的乘以1000的問題
738
+
739
+ - 新增例外處理: 若資料庫對於季資料一部分index缺失的情況下仍會盡可能去將資料蒐集並呈現
740
+
671
741
  ### 0.0.13
672
742
  - value_fetcher 新增獲得一序列評價的功能
673
743
 
@@ -1,28 +1,30 @@
1
- neurostats_API/__init__.py,sha256=UDEAcv3wrFvy1-DjCv7qON8202kc3zUGHpfClyTJWhs,20
1
+ neurostats_API/__init__.py,sha256=oEkbIWbrC6-8sBPEJQXg0QYoz3TNZtYXhSTEO6d0JcU,261
2
2
  neurostats_API/cli.py,sha256=UJSWLIw03P24p-gkBb6JSEI5dW5U12UvLf1L8HjQD-o,873
3
3
  neurostats_API/main.py,sha256=QcsfmWivg2Dnqw3MTJWiI0QvEiRs0VuH-BjwQHFCv00,677
4
- neurostats_API/fetchers/__init__.py,sha256=ylYEySHQxcAhUUWEOCGZfmaAg7Mir5MfuEhOjk3POJg,406
4
+ neurostats_API/fetchers/__init__.py,sha256=B4aBwVzf_X-YieEf3fZteU0qmBPVIB9VjrmkyWhLK18,489
5
5
  neurostats_API/fetchers/balance_sheet.py,sha256=sQv4Gk5uoKURLEdh57YknOQWiyVwaXJ2Mw75jxNqUS0,5804
6
- neurostats_API/fetchers/base.py,sha256=NW2SFzrimyAIrdJx1LVmTazelyZOAtcj54kJKHc4Vaw,1662
6
+ neurostats_API/fetchers/base.py,sha256=4YS8MJR3u9Sg6dKX7QoCYuqNeQaoYHIlvPm5x8VQ72U,4882
7
7
  neurostats_API/fetchers/cash_flow.py,sha256=TY7VAWVXkj5-mzH5Iu0sIE-oV8MvGmmDy0URNotNV1E,7614
8
8
  neurostats_API/fetchers/finance_overview.py,sha256=PxUdWY0x030olYMLcCHDBn068JLmCE2RTOce1dxs5vM,27753
9
- neurostats_API/fetchers/institution.py,sha256=rEcs5-JKbWjg8lSjG1V3YdHJQuPSN1GntnxvedESCeo,10984
9
+ neurostats_API/fetchers/institution.py,sha256=UrcBc6t7u7CnEwUsf6YmLbbJ8VncdWpq8bCz17q2dgs,11168
10
10
  neurostats_API/fetchers/margin_trading.py,sha256=lQImtNdvaBoSlKhJvQ3DkH3HjSSgKRJz4ZZpyR5-Z4I,10433
11
11
  neurostats_API/fetchers/month_revenue.py,sha256=nixX2llzjCFr2m2YVjxrSfkBusnZPrPb2dRDq1XLGhw,4251
12
- neurostats_API/fetchers/profit_lose.py,sha256=xlLNsGSy4Azf4HyZyYaX3dFad-ACO-vuQToBooZi1_w,5698
13
- neurostats_API/fetchers/tech.py,sha256=wH1kkqiETQhF0HAhk-UIiucnZ3EiL85Q-yMWCcVOiFM,11395
12
+ neurostats_API/fetchers/profit_lose.py,sha256=EN9Y0iamcAaHMZdjHXO6b_2buLnORssf8ZS7A0hi74s,5896
13
+ neurostats_API/fetchers/tech.py,sha256=Hol1bcwJ_ERcnoTXNWlqqaWOuzdl7MeiAjCvzQMZDTg,12269
14
+ neurostats_API/fetchers/tej_finance_report.py,sha256=VDP0Lx2ErCgIBBz7nbquC1ugkcnj6p7ehM2JtFInjsQ,10218
14
15
  neurostats_API/fetchers/value_invest.py,sha256=_eQxuEnIYvksb06QHixGK29Gnwr_3xmI6Tu7dv4J__E,5769
15
- neurostats_API/tools/balance_sheet.yaml,sha256=yTxrWh7m4K3LnaNunETidfNzl6S4Bf58VIg9U38XShQ,648
16
+ neurostats_API/tools/balance_sheet.yaml,sha256=6XygNG_Ybb1Xkk1e39LMLKr7ATvaCP3xxuwFbgNl6dA,673
16
17
  neurostats_API/tools/cash_flow_percentage.yaml,sha256=fk2Z4eb1JjGFvP134eJatHacB7BgTkBenhDJr83w8RE,1345
17
18
  neurostats_API/tools/finance_overview_dict.yaml,sha256=B9nV75StXkrF3yv2-eezzitlJ38eEK86RD_VY6588gQ,2884
18
- neurostats_API/tools/profit_lose.yaml,sha256=dcO-0J0BC4p06XBNuowu8ux0NTbyZiOkGfy6szHF6fw,2402
19
+ neurostats_API/tools/profit_lose.yaml,sha256=iyp9asYJ04vAxk_HBUDse_IBy5oVvYHpwsyACg5YEeg,3029
19
20
  neurostats_API/tools/seasonal_data_field_dict.txt,sha256=X8yc_el6p8BH_3FikTqBVFGsvWdXT6MHXLfKfi44334,8491
20
- neurostats_API/utils/__init__.py,sha256=FTYKRFzW2XVXdnSHXnS3mQQaHlKF9xGqrMsgZZ2kroc,142
21
- neurostats_API/utils/data_process.py,sha256=2yrO0iP1LHhF0uhXZ442PHQBI-Zd2xIqNItkNf5hKIc,6339
21
+ neurostats_API/utils/__init__.py,sha256=0tJCRmlJq2aDwcNNW-oEaA9H0OxTJMFvjpVYtG4AvZU,186
22
+ neurostats_API/utils/calculate_value.py,sha256=lUKSsWU76XRmDUcmi4eDjoQxjb3vWpAAKInF9w49VNI,782
23
+ neurostats_API/utils/data_process.py,sha256=A--dzOsu42jRxqqCD41gTtjE5rhEBYmhB6y-AnCvo5U,8986
22
24
  neurostats_API/utils/datetime.py,sha256=XJya4G8b_-ZOaBbMXgQjWh2MC4wc-o6goQ7EQJQMWrQ,773
23
25
  neurostats_API/utils/db_client.py,sha256=OYe6yazcR4Aa6jYmy47JrryUeh2NnKGqY2K_lSZe6i8,455
24
26
  neurostats_API/utils/fetcher.py,sha256=VbrUhjA-GG5AyjPX2SHtFIbZM4dm3jo0RgZzuCbb_Io,40927
25
- neurostats_API-0.0.13.dist-info/METADATA,sha256=tj8M4sSgXPprPV472mYHdEY_QOBpcObMXXo5lLGSOic,25744
26
- neurostats_API-0.0.13.dist-info/WHEEL,sha256=bFJAMchF8aTQGUgMZzHJyDDMPTO3ToJ7x23SLJa1SVo,92
27
- neurostats_API-0.0.13.dist-info/top_level.txt,sha256=nSlQPMG0VtXivJyedp4Bkf86EOy2TpW10VGxolXrqnU,15
28
- neurostats_API-0.0.13.dist-info/RECORD,,
27
+ neurostats_API-0.0.15.dist-info/METADATA,sha256=btfdGRam5QpUHFFiA_UPWYeZuAqAMYkEJ0Ufod399T4,27959
28
+ neurostats_API-0.0.15.dist-info/WHEEL,sha256=bFJAMchF8aTQGUgMZzHJyDDMPTO3ToJ7x23SLJa1SVo,92
29
+ neurostats_API-0.0.15.dist-info/top_level.txt,sha256=nSlQPMG0VtXivJyedp4Bkf86EOy2TpW10VGxolXrqnU,15
30
+ neurostats_API-0.0.15.dist-info/RECORD,,