neurostats-API 0.0.22__py3-none-any.whl → 0.0.23__py3-none-any.whl
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.
- neurostats_API/__init__.py +1 -1
- neurostats_API/fetchers/balance_sheet.py +59 -36
- neurostats_API/fetchers/base.py +6 -2
- neurostats_API/fetchers/cash_flow.py +87 -66
- neurostats_API/fetchers/finance_overview.py +26 -26
- neurostats_API/fetchers/institution.py +211 -97
- neurostats_API/fetchers/margin_trading.py +121 -94
- neurostats_API/fetchers/month_revenue.py +139 -105
- neurostats_API/fetchers/profit_lose.py +100 -80
- neurostats_API/fetchers/tech.py +61 -26
- neurostats_API/fetchers/tej_finance_report.py +23 -8
- neurostats_API/fetchers/value_invest.py +32 -12
- neurostats_API/tools/tej_db/tej_db_percent_index.yaml +0 -3
- neurostats_API/tools/tej_db/tej_db_skip_index.yaml +12 -1
- neurostats_API/tools/tej_db/tej_db_thousand_index.yaml +0 -4
- neurostats_API/utils/calculate_value.py +5 -2
- neurostats_API/utils/data_process.py +11 -5
- neurostats_API/utils/logger.py +21 -0
- {neurostats_API-0.0.22.dist-info → neurostats_API-0.0.23.dist-info}/METADATA +2 -2
- neurostats_API-0.0.23.dist-info/RECORD +35 -0
- neurostats_API-0.0.22.dist-info/RECORD +0 -34
- {neurostats_API-0.0.22.dist-info → neurostats_API-0.0.23.dist-info}/WHEEL +0 -0
- {neurostats_API-0.0.22.dist-info → neurostats_API-0.0.23.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
|
|
1
1
|
from .base import StatsFetcher
|
2
|
-
from datetime import datetime, timedelta
|
2
|
+
from datetime import datetime, timedelta, date
|
3
3
|
import json
|
4
4
|
import numpy as np
|
5
5
|
import pandas as pd
|
@@ -23,70 +23,84 @@ class MarginTradingFetcher(StatsFetcher):
|
|
23
23
|
def prepare_query(self, start_date, end_date):
|
24
24
|
pipeline = super().prepare_query()
|
25
25
|
|
26
|
-
pipeline.append(
|
27
|
-
|
28
|
-
"
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
"
|
33
|
-
"
|
34
|
-
"
|
35
|
-
"
|
36
|
-
|
37
|
-
|
38
|
-
"
|
39
|
-
"$
|
40
|
-
|
41
|
-
|
42
|
-
|
26
|
+
pipeline.append(
|
27
|
+
{
|
28
|
+
"$project": {
|
29
|
+
"_id": 0,
|
30
|
+
"ticker": 1,
|
31
|
+
"company_name": 1,
|
32
|
+
"daily_data": {
|
33
|
+
"$map": {
|
34
|
+
"input": {
|
35
|
+
"$filter": {
|
36
|
+
"input": "$daily_data",
|
37
|
+
"as": "daliy",
|
38
|
+
"cond": {
|
39
|
+
"$and": [
|
40
|
+
{
|
41
|
+
"$gte":
|
42
|
+
["$$daliy.date", start_date]
|
43
|
+
}, {
|
44
|
+
"$lte":
|
45
|
+
["$$daliy.date", end_date]
|
46
|
+
}
|
47
|
+
]
|
48
|
+
}
|
43
49
|
}
|
44
|
-
}
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
}
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
50
|
+
},
|
51
|
+
"as": "target_daliy_data",
|
52
|
+
"in": "$$target_daliy_data"
|
53
|
+
}
|
54
|
+
},
|
55
|
+
"margin_trading": {
|
56
|
+
"$map": {
|
57
|
+
"input": {
|
58
|
+
"$filter": {
|
59
|
+
"input": "$margin_trading",
|
60
|
+
"as": "margin",
|
61
|
+
"cond": {
|
62
|
+
"$and": [
|
63
|
+
{
|
64
|
+
"$gte":
|
65
|
+
["$$margin.date", start_date]
|
66
|
+
}, {
|
67
|
+
"$lte":
|
68
|
+
["$$margin.date", end_date]
|
69
|
+
}
|
70
|
+
]
|
71
|
+
}
|
62
72
|
}
|
63
|
-
}
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
}
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
73
|
+
},
|
74
|
+
"as": "target_margin_data",
|
75
|
+
"in": "$$target_margin_data"
|
76
|
+
}
|
77
|
+
},
|
78
|
+
"security_lending": {
|
79
|
+
"$map": {
|
80
|
+
"input": {
|
81
|
+
"$filter": {
|
82
|
+
"input": "$security_lending",
|
83
|
+
"as": "lending",
|
84
|
+
"cond": {
|
85
|
+
"$and": [
|
86
|
+
{
|
87
|
+
"$gte":
|
88
|
+
["$$lending.date", start_date]
|
89
|
+
}, {
|
90
|
+
"$lte":
|
91
|
+
["$$lending.date", end_date]
|
92
|
+
}
|
93
|
+
]
|
94
|
+
}
|
81
95
|
}
|
82
|
-
}
|
83
|
-
|
84
|
-
|
85
|
-
|
96
|
+
},
|
97
|
+
"as": "target_lending_data",
|
98
|
+
"in": "$$target_lending_data"
|
99
|
+
}
|
86
100
|
}
|
87
101
|
}
|
88
102
|
}
|
89
|
-
|
103
|
+
)
|
90
104
|
|
91
105
|
return pipeline
|
92
106
|
|
@@ -100,39 +114,39 @@ class MarginTradingFetcher(StatsFetcher):
|
|
100
114
|
def query_data(self):
|
101
115
|
try:
|
102
116
|
latest_time = StatsDateTime.get_latest_time(
|
103
|
-
self.ticker, self.collection
|
117
|
+
self.ticker, self.collection
|
118
|
+
)['last_update_time']
|
104
119
|
latest_date = latest_time['margin_trading']['latest_date']
|
105
|
-
end_date = latest_date.replace(
|
106
|
-
|
107
|
-
second=0,
|
108
|
-
microsecond=0)
|
109
|
-
except Exception as e:
|
110
|
-
print(
|
111
|
-
f"No updated time for institution_trading in {self.ticker}, use current time instead"
|
120
|
+
end_date = latest_date.replace(
|
121
|
+
hour=0, minute=0, second=0, microsecond=0
|
112
122
|
)
|
123
|
+
except Exception as e:
|
113
124
|
end_date = datetime.now(self.timezone)
|
114
|
-
end_date = end_date.replace(
|
115
|
-
|
116
|
-
|
117
|
-
microsecond=0)
|
125
|
+
end_date = end_date.replace(
|
126
|
+
hour=0, minute=0, second=0, microsecond=0
|
127
|
+
)
|
118
128
|
|
119
|
-
if (end_date.hour < 22):
|
129
|
+
if (end_date.hour < 22): # 拿不到今天的資料
|
120
130
|
end_date = end_date - timedelta(days=1)
|
121
131
|
|
122
132
|
start_date = end_date - timedelta(days=365)
|
123
133
|
|
124
134
|
fetched_data = self.collect_data(start_date, end_date)
|
125
135
|
|
126
|
-
fetched_data['daily_data'] = sorted(
|
127
|
-
|
128
|
-
|
129
|
-
fetched_data['margin_trading'] = sorted(
|
130
|
-
|
131
|
-
|
136
|
+
fetched_data['daily_data'] = sorted(
|
137
|
+
fetched_data['daily_data'], key=lambda x: x['date'], reverse=True
|
138
|
+
)
|
139
|
+
fetched_data['margin_trading'] = sorted(
|
140
|
+
fetched_data['margin_trading'],
|
141
|
+
key=lambda x: x['date'],
|
142
|
+
reverse=True
|
143
|
+
) if (fetched_data['margin_trading']) else []
|
144
|
+
|
132
145
|
fetched_data['security_lending'] = sorted(
|
133
146
|
fetched_data['security_lending'],
|
134
147
|
key=lambda x: x['date'],
|
135
|
-
reverse=True
|
148
|
+
reverse=True
|
149
|
+
)if (fetched_data['security_lending']) else []
|
136
150
|
|
137
151
|
table_dict = self.process_data(fetched_data)
|
138
152
|
|
@@ -149,19 +163,18 @@ class MarginTradingFetcher(StatsFetcher):
|
|
149
163
|
price_dict = {
|
150
164
|
"open": latest_data['open'],
|
151
165
|
'close': latest_data['close'],
|
152
|
-
'range': f"{latest_data['low']} - {latest_data['high']}",
|
153
|
-
'volume': latest_data['volume'] / 1000,
|
166
|
+
'range': f"{latest_data['low']:.2f} - {latest_data['high']:.2f}",
|
167
|
+
'volume': round(float(latest_data['volume']) / 1000, 2),
|
154
168
|
'last_open': yesterday_data['open'],
|
155
169
|
'last_close': yesterday_data['close'],
|
156
|
-
'last_range':
|
157
|
-
|
158
|
-
'last_volume': yesterday_data['volume'] / 1000
|
170
|
+
'last_range': f"{yesterday_data['low']:.2f} - {yesterday_data['high']:.2f}",
|
171
|
+
'last_volume': round(float(yesterday_data['volume']) / 1000, 2)
|
159
172
|
}
|
160
173
|
annual_lows = [data['low'] for data in daily_datas]
|
161
174
|
annual_highs = [data['high'] for data in daily_datas]
|
162
175
|
lowest = np.min(annual_lows).item()
|
163
176
|
highest = np.max(annual_highs).item()
|
164
|
-
price_dict['52weeks_range'] = f"{lowest} - {highest}"
|
177
|
+
price_dict['52weeks_range'] = f"{lowest:.2f} - {highest:.2f}"
|
165
178
|
|
166
179
|
return_dict['price'] = price_dict
|
167
180
|
|
@@ -169,6 +182,18 @@ class MarginTradingFetcher(StatsFetcher):
|
|
169
182
|
margin_trading = fetched_data['margin_trading']
|
170
183
|
security_lending = fetched_data['security_lending']
|
171
184
|
|
185
|
+
return_dict['margin_trading'] = pd.DataFrame()
|
186
|
+
return_dict['stock_lending'] = pd.DataFrame()
|
187
|
+
return_dict['latest_trading'] = {
|
188
|
+
'date': date.today(),
|
189
|
+
"margin_trading": pd.DataFrame(),
|
190
|
+
"stock_lending": pd.DataFrame()
|
191
|
+
}
|
192
|
+
return_dict['annual_margin'] = pd.DataFrame()
|
193
|
+
return_dict['security_offset'] = 0.0
|
194
|
+
if (not margin_trading):
|
195
|
+
return return_dict
|
196
|
+
|
172
197
|
latest_margin_date = margin_trading[0]['date']
|
173
198
|
latest_lending_date = security_lending[0]['date']
|
174
199
|
## 融資融券
|
@@ -176,7 +201,8 @@ class MarginTradingFetcher(StatsFetcher):
|
|
176
201
|
for trading in margin_trading:
|
177
202
|
trading['financing']['現償'] = trading['financing'].pop('現金償還')
|
178
203
|
trading['short_selling']['現償'] = trading['short_selling'].pop(
|
179
|
-
'現券償還'
|
204
|
+
'現券償還'
|
205
|
+
)
|
180
206
|
### 轉換
|
181
207
|
latest_margin_trading = margin_trading[0]
|
182
208
|
latest_margin_trading_df = {
|
@@ -185,7 +211,8 @@ class MarginTradingFetcher(StatsFetcher):
|
|
185
211
|
if (isinstance(sub_dict, dict))
|
186
212
|
}
|
187
213
|
latest_margin_trading_df = pd.DataFrame.from_dict(
|
188
|
-
latest_margin_trading_df
|
214
|
+
latest_margin_trading_df
|
215
|
+
)
|
189
216
|
|
190
217
|
## 借券表格
|
191
218
|
latest_stock_lending = security_lending[0]['stock_lending']
|
@@ -195,9 +222,9 @@ class MarginTradingFetcher(StatsFetcher):
|
|
195
222
|
for type_name, value in latest_stock_lending.items()
|
196
223
|
}
|
197
224
|
latest_stock_lending.pop("前日餘額")
|
198
|
-
latest_stock_lending_df = pd.DataFrame.from_dict(
|
199
|
-
|
200
|
-
|
225
|
+
latest_stock_lending_df = pd.DataFrame.from_dict(
|
226
|
+
latest_stock_lending, orient="index", columns=['stock_lending']
|
227
|
+
)
|
201
228
|
|
202
229
|
latest_dict = {
|
203
230
|
"date": latest_margin_date,
|
@@ -262,14 +289,14 @@ class MarginTradingFetcher(StatsFetcher):
|
|
262
289
|
|
263
290
|
annual_dict = {
|
264
291
|
date: {
|
265
|
-
"close": close_prices
|
266
|
-
"volume": volumes
|
292
|
+
"close": close_prices.get(date, 0.0),
|
293
|
+
"volume": volumes.get(date, 0.0),
|
267
294
|
**financings[date],
|
268
295
|
**short_sellings[date],
|
269
296
|
**stock_lendings[date],
|
270
|
-
"資券互抵":security_offsets[date]
|
297
|
+
"資券互抵": security_offsets[date]
|
271
298
|
}
|
272
|
-
for date in
|
299
|
+
for date in financings.keys()
|
273
300
|
}
|
274
301
|
|
275
302
|
annual_table = pd.DataFrame.from_dict(annual_dict)
|
@@ -4,6 +4,8 @@ import pandas as pd
|
|
4
4
|
from ..utils import StatsDateTime, StatsProcessor, YoY_Calculator
|
5
5
|
import importlib.resources as pkg_resources
|
6
6
|
import yaml
|
7
|
+
import traceback
|
8
|
+
import logging
|
7
9
|
|
8
10
|
|
9
11
|
class MonthRevenueFetcher(StatsFetcher):
|
@@ -11,151 +13,183 @@ class MonthRevenueFetcher(StatsFetcher):
|
|
11
13
|
iFa.ai: 財務分析 -> 每月營收
|
12
14
|
"""
|
13
15
|
|
14
|
-
def __init__(self, ticker, db_client):
|
16
|
+
def __init__(self, ticker, db_client, logger = None):
|
15
17
|
super().__init__(ticker, db_client)
|
18
|
+
self.logger = logger or logging.getLogger(__name__)
|
16
19
|
|
17
|
-
def
|
20
|
+
def _prepare_query(self, target_year, target_month):
|
18
21
|
pipeline = super().prepare_query()
|
19
22
|
|
20
|
-
pipeline.append(
|
21
|
-
|
22
|
-
"
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
"
|
27
|
-
"
|
28
|
-
|
29
|
-
"
|
30
|
-
|
23
|
+
pipeline.append(
|
24
|
+
{
|
25
|
+
"$project": {
|
26
|
+
"_id": 0,
|
27
|
+
"ticker": 1,
|
28
|
+
"company_name": 1,
|
29
|
+
"monthly_data": {
|
30
|
+
"$sortArray": {
|
31
|
+
"input": "$monthly_data",
|
32
|
+
"sortBy": {
|
33
|
+
"year": -1,
|
34
|
+
"month": -1
|
35
|
+
}
|
31
36
|
}
|
32
|
-
}
|
33
|
-
}
|
37
|
+
},
|
38
|
+
}
|
34
39
|
}
|
35
|
-
|
40
|
+
)
|
36
41
|
|
37
42
|
return pipeline
|
38
43
|
|
39
44
|
def collect_data(self, target_year, target_month):
|
40
|
-
pipeline = self.
|
41
|
-
|
45
|
+
pipeline = self._prepare_query(target_year, target_month)
|
42
46
|
fetched_data = self.collection.aggregate(pipeline)
|
47
|
+
fetched_data = fetched_data.to_list()
|
43
48
|
|
44
|
-
|
45
|
-
|
46
|
-
return fetched_data[-1]
|
49
|
+
return fetched_data
|
47
50
|
|
48
51
|
def query_data(self):
|
49
|
-
|
50
|
-
latest_time = StatsDateTime.get_latest_time(
|
51
|
-
self.ticker, self.collection)['last_update_time']
|
52
|
-
target_year = latest_time['monthly_data']['latest_year']
|
53
|
-
target_month = latest_time['monthly_data']['latest_month']
|
54
|
-
except Exception as e:
|
55
|
-
today = StatsDateTime.get_today()
|
56
|
-
target_month = today.month
|
57
|
-
target_year = today.year
|
52
|
+
target_year, target_month = self._get_target_year_and_month()
|
58
53
|
|
59
54
|
# Query data
|
60
55
|
fetched_data = self.collect_data(target_year, target_month)
|
61
56
|
|
62
|
-
|
57
|
+
try:
|
58
|
+
return self._process_data(fetched_data[-1])
|
59
|
+
except Exception:
|
60
|
+
recent_date = []
|
61
|
+
for _ in range(12):
|
62
|
+
recent_date.append(f"{target_year}/{target_month}")
|
63
|
+
target_year, target_month = (
|
64
|
+
target_year - 1, 12
|
65
|
+
) if target_month == 1 else (target_year, target_month - 1)
|
66
|
+
|
67
|
+
# logging.warning(f"{self.ticker}: No monthly revenue data in TWSE mongoDB", exc_info=True)
|
68
|
+
return self._get_empty_structure(target_year, target_month)
|
69
|
+
|
70
|
+
def _process_data(self, fetched_data):
|
63
71
|
|
64
|
-
|
72
|
+
monthly_data = fetched_data.get('monthly_data', [])
|
73
|
+
if not monthly_data:
|
74
|
+
raise ValueError("monthly_data is empty or missing")
|
65
75
|
|
66
|
-
monthly_data = fetched_data['monthly_data']
|
67
76
|
for data in monthly_data:
|
68
77
|
for key, value in data.items():
|
69
|
-
if
|
78
|
+
if "YoY" in key:
|
70
79
|
data[key] = StatsProcessor.cal_percentage(value)
|
71
|
-
elif
|
72
|
-
data[key] = StatsProcessor.cal_non_percentage(value,
|
73
|
-
|
74
|
-
|
75
|
-
elif (key not in ('year', 'month')):
|
76
|
-
data[key] = StatsProcessor.cal_non_percentage(value,
|
77
|
-
postfix="千元")
|
78
|
-
target_month = monthly_data[0]['month']
|
79
|
-
monthly_df = pd.DataFrame(monthly_data)
|
80
|
+
elif "ratio" in key or "percentage" in key:
|
81
|
+
data[key] = StatsProcessor.cal_non_percentage(value, to_str=True, postfix="%")
|
82
|
+
elif key not in ('year', 'month'):
|
83
|
+
data[key] = StatsProcessor.cal_non_percentage(value, postfix="千元")
|
80
84
|
|
85
|
+
monthly_df = pd.DataFrame(monthly_data)
|
86
|
+
target_month = monthly_data[0]['month']
|
81
87
|
target_month_df = monthly_df[monthly_df['month'] == target_month]
|
82
88
|
annual_month_df = monthly_df[monthly_df['month'] == 12]
|
83
|
-
month_revenue_df = monthly_df.pivot(index='month',
|
84
|
-
columns='year',
|
85
|
-
values='revenue')
|
86
|
-
|
87
|
-
grand_total_df = target_month_df.pivot(index='month',
|
88
|
-
columns='year',
|
89
|
-
values='grand_total')
|
90
89
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
grand_total_df.rename(index={target_month: f"grand_total"},
|
96
|
-
inplace=True)
|
90
|
+
|
91
|
+
month_revenue_df = monthly_df.pivot(
|
92
|
+
index='month', columns='year', values='revenue'
|
93
|
+
)
|
97
94
|
month_revenue_df = month_revenue_df.sort_index(ascending=False)
|
98
|
-
month_revenue_df = pd.concat([grand_total_df, month_revenue_df],
|
99
|
-
axis=0)
|
100
|
-
|
101
|
-
fetched_data['month_revenue'] = month_revenue_df[sorted(
|
102
|
-
month_revenue_df.columns, reverse=True)]
|
103
|
-
# 歷年月營收
|
104
|
-
fetched_data[
|
105
|
-
'this_month_revenue_over_years'] = target_month_df.set_index(
|
106
|
-
"year")[[
|
107
|
-
"revenue", "revenue_increment_ratio", "YoY_1", "YoY_3",
|
108
|
-
"YoY_5", "YoY_10"
|
109
|
-
]].T
|
110
|
-
# 歷年營收成長量
|
111
|
-
fetched_data['grand_total_over_years'] = target_month_df.set_index(
|
112
|
-
"year")[[
|
113
|
-
"grand_total", "grand_total_increment_ratio",
|
114
|
-
"grand_total_YoY_1", "grand_total_YoY_3", "grand_total_YoY_5",
|
115
|
-
"grand_total_YoY_10"
|
116
|
-
]].T
|
117
95
|
|
118
|
-
|
96
|
+
grand_total_df = target_month_df.pivot(
|
97
|
+
index='month', columns='year', values='grand_total'
|
98
|
+
)
|
99
|
+
grand_total_df.rename(
|
100
|
+
index={target_month: f"grand_total"}, inplace=True
|
101
|
+
)
|
102
|
+
month_revenue_df = pd.concat([grand_total_df, month_revenue_df], axis=0)
|
119
103
|
|
120
|
-
|
121
|
-
|
104
|
+
annual_total_df = annual_month_df.pivot(
|
105
|
+
index='month', columns='year', values='grand_total'
|
106
|
+
)
|
107
|
+
|
108
|
+
fetched_data.update(
|
109
|
+
{
|
110
|
+
"month_revenue": month_revenue_df[sorted(month_revenue_df.columns, reverse=True)],
|
111
|
+
"this_month_revenue_over_years": target_month_df.set_index("year")[[
|
112
|
+
"revenue", "revenue_increment_ratio", "YoY_1", "YoY_3", "YoY_5", "YoY_10"
|
113
|
+
]].T,
|
114
|
+
"grand_total_over_years": target_month_df.set_index("year")[[
|
115
|
+
"grand_total", "grand_total_increment_ratio", "grand_total_YoY_1", "grand_total_YoY_3", "grand_total_YoY_5", "grand_total_YoY_10"
|
116
|
+
]].T,
|
117
|
+
"recent_month_revenue": self._get_recent_growth(monthly_data, grand_total_dict=annual_total_df.to_dict(), interval=12)
|
118
|
+
}
|
122
119
|
)
|
123
120
|
|
121
|
+
fetched_data.pop("monthly_data")
|
124
122
|
return fetched_data
|
125
123
|
|
126
|
-
def
|
127
|
-
|
124
|
+
def _get_recent_growth(self, monthly_data, grand_total_dict, interval=12):
|
125
|
+
last_month_data = monthly_data[1:interval + 1] + [{}] * max(0, interval - len(monthly_data) + 1)
|
128
126
|
|
129
127
|
MoMs = [
|
130
|
-
YoY_Calculator.cal_growth(
|
131
|
-
for
|
132
|
-
recent_month_data[:12], recent_month_data[1:13]
|
133
|
-
)
|
128
|
+
YoY_Calculator.cal_growth(this.get('revenue'), last.get('revenue'), delta = 1)
|
129
|
+
for this, last in zip(monthly_data[:interval], last_month_data[:interval])
|
134
130
|
]
|
135
131
|
|
136
|
-
|
137
|
-
"date" : [f"{data['year']}/{data['month']}" for data in recent_month_data[:interval]],
|
138
|
-
"revenue" : [data['revenue'] for data in recent_month_data[:interval]],
|
139
|
-
"MoM" : [f"{(data * 100):.2f}%" for data in MoMs],
|
140
|
-
"YoY" : [f"{data['revenue_increment_ratio']}" for data in recent_month_data[:interval]],
|
141
|
-
"total_YoY": [f"{data['grand_total_increment_ratio']}" for data in recent_month_data[:interval]],
|
142
|
-
}
|
143
|
-
|
144
|
-
# accum_YoY
|
145
|
-
# accum_YoY 為 Davis提出的定義
|
146
|
-
# 2024/6的累計YoY(accum_YoY) 為 2024累計到6月為止的總營收/2023年度總營收
|
147
|
-
accum_YoYs = []
|
148
|
-
for data in monthly_data[:interval]:
|
132
|
+
def safe_accum_yoy(data):
|
149
133
|
try:
|
150
134
|
year = data['year'] - 1
|
151
135
|
total = grand_total_dict[year][12]
|
152
|
-
|
153
|
-
|
154
|
-
except Exception
|
155
|
-
|
156
|
-
|
157
|
-
recent_month_data['accum_YoY'] = accum_YoYs
|
136
|
+
grand_total = data.get('grand_total')
|
137
|
+
return f"{round(((grand_total - total) / total) * 100, 2)}%"
|
138
|
+
except Exception:
|
139
|
+
self.logger.debug(f"accum_YoY calc failed for year={data.get('year')} / ticker={self.ticker}", exc_info=True)
|
140
|
+
return None
|
158
141
|
|
159
|
-
|
142
|
+
recent_month_data = {
|
143
|
+
"date": [f"{d.get('year', 0)}/{d.get('month', 0)}" for d in monthly_data[:interval]],
|
144
|
+
"revenue": [d.get('revenue') for d in monthly_data[:interval]],
|
145
|
+
"MoM": [f"{(m * 100):.2f}%" if isinstance(m, float) else None for m in MoMs],
|
146
|
+
"YoY": [d.get('revenue_increment_ratio') for d in monthly_data[:interval]],
|
147
|
+
"total_YoY": [d.get('grand_total_increment_ratio') for d in monthly_data[:interval]],
|
148
|
+
# accum_YoY
|
149
|
+
# accum_YoY 為 Davis提出的定義
|
150
|
+
# 2024/6的累計YoY(accum_YoY) 為 2024累計到6月為止的總營收/2023年度總營收
|
151
|
+
"accum_YoY": [safe_accum_yoy(d) for d in monthly_data[:interval]]
|
152
|
+
}
|
160
153
|
|
161
|
-
|
154
|
+
df = pd.DataFrame(recent_month_data)
|
155
|
+
return df[df['date'] != "0/0"].set_index('date').T
|
156
|
+
|
157
|
+
|
158
|
+
def _get_empty_structure(self, target_year, target_month):
|
159
|
+
"""
|
160
|
+
Exception 發生時回傳
|
161
|
+
"""
|
162
|
+
recent_date = [f"{target_year}/{target_month}"]
|
163
|
+
for _ in range(11):
|
164
|
+
target_year, target_month = (target_year - 1, 12) if target_month == 1 else (target_year, target_month - 1)
|
165
|
+
recent_date.append(f"{target_year}/{target_month}")
|
166
|
+
|
167
|
+
def empty_df(index, columns):
|
168
|
+
return pd.DataFrame(index=index, columns=columns)
|
169
|
+
|
170
|
+
return {
|
171
|
+
"month_revenue": empty_df(
|
172
|
+
index=pd.Index(['grand_total', 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], dtype='object', name='month'),
|
173
|
+
columns=pd.Index([f"{target_year - i}" for i in range(10)], dtype=object, name='year')
|
174
|
+
),
|
175
|
+
"this_month_revenue_over_years": empty_df(
|
176
|
+
index=pd.Index(['revenue', 'revenue_increment_ratio', 'YoY_1', 'YoY_3', 'YoY_5', 'YoY_10'], dtype='object'),
|
177
|
+
columns=pd.Index([f"{target_year - i}" for i in range(10)], dtype='int64', name='year')
|
178
|
+
),
|
179
|
+
"grand_total_over_years": empty_df(
|
180
|
+
index=pd.Index(['grand_total', 'grand_total_increment_ratio', 'grand_total_YoY_1', 'grand_total_YoY_3', 'grand_total_YoY_5', 'grand_total_YoY_10'], dtype='object'),
|
181
|
+
columns=pd.Index([f"{target_year - i}" for i in range(10)], dtype='int64', name='year')
|
182
|
+
),
|
183
|
+
"recent_month_revenue": empty_df(
|
184
|
+
index=pd.Index(['revenue', 'MoM', 'YoY', 'total_YoY', 'accum_YoY'], dtype='object'),
|
185
|
+
columns=pd.Index([], dtype = 'object', name = 'date')
|
186
|
+
)
|
187
|
+
}
|
188
|
+
|
189
|
+
def _get_target_year_and_month(self):
|
190
|
+
try:
|
191
|
+
latest_time = StatsDateTime.get_latest_time(self.ticker, self.collection)['last_update_time']
|
192
|
+
return latest_time['monthly_data']['latest_year'], latest_time['monthly_data']['latest_month']
|
193
|
+
except Exception:
|
194
|
+
today = StatsDateTime.get_today()
|
195
|
+
return today.year, today.month
|