neurostats-API 0.0.25rc1__py3-none-any.whl → 1.0.0rc1__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/async_mode/__init__.py +13 -0
- neurostats_API/async_mode/db/__init__.py +3 -0
- neurostats_API/async_mode/db/base.py +24 -0
- neurostats_API/async_mode/db/tej.py +10 -0
- neurostats_API/async_mode/db/twse.py +8 -0
- neurostats_API/async_mode/db/us.py +9 -0
- neurostats_API/async_mode/db_extractors/__init__.py +20 -0
- neurostats_API/async_mode/db_extractors/base.py +66 -0
- neurostats_API/async_mode/db_extractors/daily/__init__.py +7 -0
- neurostats_API/async_mode/db_extractors/daily/base.py +89 -0
- neurostats_API/async_mode/db_extractors/daily/tej_chip.py +14 -0
- neurostats_API/async_mode/db_extractors/daily/tej_tech.py +12 -0
- neurostats_API/async_mode/db_extractors/daily/twse_chip.py +44 -0
- neurostats_API/async_mode/db_extractors/daily/value.py +93 -0
- neurostats_API/async_mode/db_extractors/daily/yf.py +12 -0
- neurostats_API/async_mode/db_extractors/month_revenue/__init__.py +1 -0
- neurostats_API/async_mode/db_extractors/month_revenue/base.py +140 -0
- neurostats_API/async_mode/db_extractors/month_revenue/twse.py +5 -0
- neurostats_API/async_mode/db_extractors/seasonal/__init__.py +4 -0
- neurostats_API/async_mode/db_extractors/seasonal/balance_sheet.py +19 -0
- neurostats_API/async_mode/db_extractors/seasonal/base.py +159 -0
- neurostats_API/async_mode/db_extractors/seasonal/cashflow.py +10 -0
- neurostats_API/async_mode/db_extractors/seasonal/profit_lose.py +17 -0
- neurostats_API/async_mode/db_extractors/seasonal/tej.py +87 -0
- neurostats_API/async_mode/factory/__init__.py +1 -0
- neurostats_API/async_mode/factory/extractor_factory.py +168 -0
- neurostats_API/async_mode/factory/transformer_factory.py +164 -0
- neurostats_API/async_mode/fetchers/__init__.py +10 -0
- neurostats_API/async_mode/fetchers/balance_sheet.py +31 -0
- neurostats_API/async_mode/fetchers/base.py +48 -0
- neurostats_API/async_mode/fetchers/cash_flow.py +56 -0
- neurostats_API/async_mode/fetchers/finance_overview.py +134 -0
- neurostats_API/async_mode/fetchers/month_revenue.py +35 -0
- neurostats_API/async_mode/fetchers/profit_lose.py +46 -0
- neurostats_API/async_mode/fetchers/tech.py +205 -0
- neurostats_API/async_mode/fetchers/tej.py +88 -0
- neurostats_API/async_mode/fetchers/twse_institution.py +62 -0
- neurostats_API/async_mode/fetchers/twse_margin.py +100 -0
- neurostats_API/async_mode/fetchers/value.py +76 -0
- neurostats_API/config/company_list/ticker_index_industry_map.json +7946 -0
- neurostats_API/config/company_list/us.json +9986 -0
- neurostats_API/{tools → config}/tej_db/tej_db_skip_index.yaml +0 -2
- neurostats_API/{tools → config}/twse/profit_lose.yaml +0 -6
- neurostats_API/fetchers/finance_overview.py +27 -5
- neurostats_API/transformers/__init__.py +40 -0
- neurostats_API/transformers/balance_sheet/__init__.py +2 -0
- neurostats_API/transformers/balance_sheet/base.py +51 -0
- neurostats_API/transformers/balance_sheet/twse.py +76 -0
- neurostats_API/transformers/balance_sheet/us.py +30 -0
- neurostats_API/transformers/base.py +110 -0
- neurostats_API/transformers/cash_flow/__init__.py +2 -0
- neurostats_API/transformers/cash_flow/base.py +114 -0
- neurostats_API/transformers/cash_flow/twse.py +68 -0
- neurostats_API/transformers/cash_flow/us.py +38 -0
- neurostats_API/transformers/daily_chip/__init__.py +1 -0
- neurostats_API/transformers/daily_chip/base.py +5 -0
- neurostats_API/transformers/daily_chip/tej.py +0 -0
- neurostats_API/transformers/daily_chip/twse_chip.py +412 -0
- neurostats_API/transformers/daily_chip/utils/__init__.py +0 -0
- neurostats_API/transformers/daily_chip/utils/institution.py +90 -0
- neurostats_API/transformers/daily_chip/utils/margin_trading.py +2 -0
- neurostats_API/transformers/daily_chip/utils/security_lending.py +0 -0
- neurostats_API/transformers/daily_tech/__init__.py +1 -0
- neurostats_API/transformers/daily_tech/base.py +5 -0
- neurostats_API/transformers/daily_tech/tech.py +84 -0
- neurostats_API/transformers/daily_tech/utils/__init__.py +1 -0
- neurostats_API/transformers/daily_tech/utils/processor.py +251 -0
- neurostats_API/transformers/finance_overview/__init__.py +2 -0
- neurostats_API/transformers/finance_overview/agent_overview.py +55 -0
- neurostats_API/transformers/finance_overview/base.py +824 -0
- neurostats_API/transformers/finance_overview/stats_overview.py +64 -0
- neurostats_API/transformers/month_revenue/__init__.py +1 -0
- neurostats_API/transformers/month_revenue/base.py +60 -0
- neurostats_API/transformers/month_revenue/twse.py +129 -0
- neurostats_API/transformers/profit_lose/__init__.py +2 -0
- neurostats_API/transformers/profit_lose/base.py +82 -0
- neurostats_API/transformers/profit_lose/twse.py +133 -0
- neurostats_API/transformers/profit_lose/us.py +25 -0
- neurostats_API/transformers/tej/__init__.py +1 -0
- neurostats_API/transformers/tej/base.py +149 -0
- neurostats_API/transformers/tej/finance_statement.py +80 -0
- neurostats_API/transformers/value/__init__.py +1 -0
- neurostats_API/transformers/value/base.py +5 -0
- neurostats_API/transformers/value/tej.py +8 -0
- neurostats_API/transformers/value/twse.py +48 -0
- neurostats_API/utils/__init__.py +1 -1
- neurostats_API/utils/data_process.py +10 -6
- neurostats_API/utils/exception.py +8 -0
- neurostats_API/utils/logger.py +21 -0
- neurostats_API-1.0.0rc1.dist-info/METADATA +102 -0
- neurostats_API-1.0.0rc1.dist-info/RECORD +119 -0
- neurostats_API-0.0.25rc1.dist-info/METADATA +0 -858
- neurostats_API-0.0.25rc1.dist-info/RECORD +0 -36
- /neurostats_API/{tools → config}/company_list/tw.json +0 -0
- /neurostats_API/{tools → config}/company_list/us_TradingView_list.json +0 -0
- /neurostats_API/{tools → config}/tej_db/tej_db_index.yaml +0 -0
- /neurostats_API/{tools → config}/tej_db/tej_db_percent_index.yaml +0 -0
- /neurostats_API/{tools → config}/tej_db/tej_db_thousand_index.yaml +0 -0
- /neurostats_API/{tools → config}/twse/balance_sheet.yaml +0 -0
- /neurostats_API/{tools → config}/twse/cash_flow_percentage.yaml +0 -0
- /neurostats_API/{tools → config}/twse/finance_overview_dict.yaml +0 -0
- /neurostats_API/{tools → config}/twse/seasonal_data_field_dict.txt +0 -0
- {neurostats_API-0.0.25rc1.dist-info → neurostats_API-1.0.0rc1.dist-info}/WHEEL +0 -0
- {neurostats_API-0.0.25rc1.dist-info → neurostats_API-1.0.0rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
from .base import BaseFinanceOverviewTransformer, FinanceOverviewProcessor
|
2
|
+
from neurostats_API.utils import StatsProcessor
|
3
|
+
|
4
|
+
|
5
|
+
class FinanceOverviewTransformer(BaseFinanceOverviewTransformer):
|
6
|
+
|
7
|
+
def __init__(self, ticker, company_name, zone):
|
8
|
+
super().__init__(ticker, company_name, zone)
|
9
|
+
|
10
|
+
self.target_fields = StatsProcessor.load_yaml(
|
11
|
+
"twse/finance_overview_dict.yaml"
|
12
|
+
)
|
13
|
+
self.inverse_dict = StatsProcessor.load_txt(
|
14
|
+
"twse/seasonal_data_field_dict.txt", json_load=True
|
15
|
+
)
|
16
|
+
|
17
|
+
def process_transform(self, balance_sheet, profit_lose, cash_flow):
|
18
|
+
|
19
|
+
processed_data_dict = self._filter_target_data(
|
20
|
+
balance_sheet, profit_lose, cash_flow
|
21
|
+
)
|
22
|
+
FinanceOverviewProcessor.process_rate(processed_data_dict) # 處理百分比
|
23
|
+
FinanceOverviewProcessor.process_all(processed_data_dict) # 計算所有指標公式
|
24
|
+
self._fill_nan_index(processed_data_dict) # 處理空值
|
25
|
+
FinanceOverviewProcessor.process_thousand_dollar(processed_data_dict) # 處理千元單位
|
26
|
+
|
27
|
+
return processed_data_dict
|
28
|
+
|
29
|
+
def _filter_target_data(self, balance_sheet, profit_lose, cash_flow):
|
30
|
+
data_dict = {
|
31
|
+
'balance_sheet': balance_sheet[-1]['balance_sheet'],
|
32
|
+
'profit_lose': profit_lose[-1]['profit_lose'],
|
33
|
+
"cash_flow": cash_flow[-1]['cash_flow']
|
34
|
+
}
|
35
|
+
filtered_dict = {}
|
36
|
+
|
37
|
+
for key, target_sets in self.target_fields.items():
|
38
|
+
try:
|
39
|
+
small_targets = target_sets['field']
|
40
|
+
value_index = target_sets['value']
|
41
|
+
|
42
|
+
for small_target in small_targets:
|
43
|
+
big_target = self.inverse_dict[small_target]
|
44
|
+
# 透過inverse_dict取得項目在balance_sheet/profit_lose/cash_flow
|
45
|
+
|
46
|
+
if (small_target == "利息費用_bank"):
|
47
|
+
small_target = small_target[:small_target.find("_bank")]
|
48
|
+
|
49
|
+
seasonal_data = data_dict.get(big_target, {}) # 對應的列表
|
50
|
+
|
51
|
+
filtered_dict[key] = seasonal_data.get(small_target, {}).get(
|
52
|
+
value_index, None
|
53
|
+
) # 取對應的值
|
54
|
+
|
55
|
+
except Exception as e:
|
56
|
+
print(f"filter failed :{e}")
|
57
|
+
continue
|
58
|
+
|
59
|
+
return filtered_dict
|
60
|
+
|
61
|
+
def _fill_nan_index(self, finance_dict):
|
62
|
+
for key in self.target_fields.keys():
|
63
|
+
if (key not in finance_dict.keys()):
|
64
|
+
finance_dict[key] = None
|
@@ -0,0 +1 @@
|
|
1
|
+
from .twse import TWSEMonthlyRevenueTransformer
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import abc
|
2
|
+
from ..base import BaseTransformer
|
3
|
+
from neurostats_API.utils import StatsProcessor
|
4
|
+
import pandas as pd
|
5
|
+
|
6
|
+
class BaseMonthRevenueTransformer(BaseTransformer):
|
7
|
+
def __init__(self, ticker, company_name, zone):
|
8
|
+
self.ticker = ticker
|
9
|
+
self.company_name = company_name,
|
10
|
+
self.zone = zone
|
11
|
+
|
12
|
+
self.return_keys = []
|
13
|
+
|
14
|
+
@abc.abstractmethod
|
15
|
+
def process_transform(self):
|
16
|
+
pass
|
17
|
+
|
18
|
+
@staticmethod
|
19
|
+
def _process_unit(data_df, postfix):
|
20
|
+
|
21
|
+
lambda_map = {
|
22
|
+
"revenue": lambda x: StatsProcessor.cal_non_percentage(x, postfix="千元"),
|
23
|
+
"increment": lambda x : StatsProcessor.cal_non_percentage(x, postfix="千元"),
|
24
|
+
"ratio": lambda x : StatsProcessor.cal_non_percentage(x, to_str=True, postfix="%"),
|
25
|
+
'percentage': lambda x : StatsProcessor.cal_non_percentage(x, to_str=True, postfix="%"),
|
26
|
+
"YoY_1": StatsProcessor.cal_percentage,
|
27
|
+
"YoY_3": StatsProcessor.cal_percentage,
|
28
|
+
"YoY_5": StatsProcessor.cal_percentage,
|
29
|
+
"YoY_10": StatsProcessor.cal_percentage
|
30
|
+
}
|
31
|
+
|
32
|
+
process_fn = lambda_map.get(postfix)
|
33
|
+
postfix_cols = data_df.columns.str.endswith(postfix)
|
34
|
+
postfix_cols = data_df.loc[:, postfix_cols].columns
|
35
|
+
|
36
|
+
data_df[postfix_cols] = (
|
37
|
+
data_df[postfix_cols].map(
|
38
|
+
process_fn
|
39
|
+
)
|
40
|
+
)
|
41
|
+
|
42
|
+
return data_df
|
43
|
+
|
44
|
+
def _apply_process_unit_pipeline(
|
45
|
+
self,
|
46
|
+
data_df,
|
47
|
+
postfix_list = ['revenue', 'increment']
|
48
|
+
):
|
49
|
+
return super()._apply_process_unit_pipeline(
|
50
|
+
data_df,
|
51
|
+
postfix_list
|
52
|
+
)
|
53
|
+
|
54
|
+
def _process_data(self, fetched_data):
|
55
|
+
for data in fetched_data:
|
56
|
+
data.pop("ticker")
|
57
|
+
data.pop("company_name")
|
58
|
+
data.pop("memo")
|
59
|
+
|
60
|
+
return pd.DataFrame(fetched_data)
|
@@ -0,0 +1,129 @@
|
|
1
|
+
from .base import BaseMonthRevenueTransformer
|
2
|
+
from neurostats_API.utils import StatsProcessor, YoY_Calculator
|
3
|
+
import pandas as pd
|
4
|
+
|
5
|
+
class TWSEMonthlyRevenueTransformer(BaseMonthRevenueTransformer):
|
6
|
+
def __init__(self, ticker, company_name, zone):
|
7
|
+
super().__init__(ticker, company_name, zone)
|
8
|
+
|
9
|
+
self.data_df = None
|
10
|
+
def process_transform(self, fetched_data):
|
11
|
+
self.data_df = self._process_data(fetched_data)
|
12
|
+
target_month = fetched_data[0]['month']
|
13
|
+
|
14
|
+
self._apply_process_unit_pipeline(
|
15
|
+
self.data_df,
|
16
|
+
postfix_list = [
|
17
|
+
'YoY_1',
|
18
|
+
'YoY_3',
|
19
|
+
"YoY_5",
|
20
|
+
"YoY_10",
|
21
|
+
"ratio",
|
22
|
+
"percentage",
|
23
|
+
"increment",
|
24
|
+
"revenue"
|
25
|
+
]
|
26
|
+
)
|
27
|
+
|
28
|
+
target_month_df = self.data_df[self.data_df['month'] == target_month]
|
29
|
+
annual_month_df = self.data_df[self.data_df['month'] == 12]
|
30
|
+
|
31
|
+
month_revenue_df = self.data_df.pivot(
|
32
|
+
index='month', columns='year', values='revenue'
|
33
|
+
)
|
34
|
+
month_revenue_df = month_revenue_df.sort_index(ascending=False)
|
35
|
+
|
36
|
+
grand_total_df = target_month_df.pivot(
|
37
|
+
index='month', columns='year', values='grand_total'
|
38
|
+
)
|
39
|
+
grand_total_df.rename(
|
40
|
+
index={target_month: f"grand_total"}, inplace=True
|
41
|
+
)
|
42
|
+
month_revenue_df = pd.concat([grand_total_df, month_revenue_df], axis=0)
|
43
|
+
|
44
|
+
annual_total_df = annual_month_df.pivot(
|
45
|
+
index='month', columns='year', values='grand_total'
|
46
|
+
)
|
47
|
+
|
48
|
+
target_month_df = target_month_df.set_index("year").T
|
49
|
+
|
50
|
+
return_dict = {
|
51
|
+
"month_revenue": month_revenue_df[sorted(month_revenue_df.columns, reverse=True)],
|
52
|
+
"this_month_revenue_over_years": target_month_df.loc[[
|
53
|
+
"revenue", "revenue_increment_ratio", "YoY_1", "YoY_3", "YoY_5", "YoY_10"
|
54
|
+
]],
|
55
|
+
"grand_total_over_years": target_month_df.loc[[
|
56
|
+
"grand_total", "grand_total_increment_ratio", "grand_total_YoY_1", "grand_total_YoY_3", "grand_total_YoY_5", "grand_total_YoY_10"
|
57
|
+
]],
|
58
|
+
"recent_month_revenue": self._get_recent_growth(
|
59
|
+
fetched_data,
|
60
|
+
grand_total_dict=annual_total_df.to_dict(),
|
61
|
+
interval=12
|
62
|
+
)
|
63
|
+
}
|
64
|
+
|
65
|
+
return return_dict
|
66
|
+
|
67
|
+
def _get_recent_growth(self, monthly_data, grand_total_dict, interval=12):
|
68
|
+
last_month_data = monthly_data[1:interval + 1] + [{}] * max(0, interval - len(monthly_data) + 1)
|
69
|
+
|
70
|
+
MoMs = [
|
71
|
+
YoY_Calculator.cal_growth(this.get('revenue'), last.get('revenue'), delta = 1)
|
72
|
+
for this, last in zip(monthly_data[:interval], last_month_data[:interval])
|
73
|
+
]
|
74
|
+
|
75
|
+
def safe_accum_yoy(data):
|
76
|
+
try:
|
77
|
+
year = data['year'] - 1
|
78
|
+
total = grand_total_dict[year][12]
|
79
|
+
grand_total = data.get('grand_total')
|
80
|
+
return f"{round(((grand_total - total) / total) * 100, 2)}%"
|
81
|
+
except Exception:
|
82
|
+
return None
|
83
|
+
|
84
|
+
recent_month_data = {
|
85
|
+
"date": [f"{d.get('year', 0)}/{d.get('month', 0)}" for d in monthly_data[:interval]],
|
86
|
+
"revenue": [d.get('revenue') for d in monthly_data[:interval]],
|
87
|
+
"MoM": [f"{(m * 100):.2f}%" if isinstance(m, float) else None for m in MoMs],
|
88
|
+
"YoY": [d.get('revenue_increment_ratio') for d in monthly_data[:interval]],
|
89
|
+
"total_YoY": [d.get('grand_total_increment_ratio') for d in monthly_data[:interval]],
|
90
|
+
# accum_YoY
|
91
|
+
# accum_YoY 為 Davis提出的定義
|
92
|
+
# 2024/6的累計YoY(accum_YoY) 為 2024累計到6月為止的總營收/2023年度總營收
|
93
|
+
"accum_YoY": [safe_accum_yoy(d) for d in monthly_data[:interval]]
|
94
|
+
}
|
95
|
+
|
96
|
+
df = pd.DataFrame(recent_month_data)
|
97
|
+
return df[df['date'] != "0/0"].set_index('date').T
|
98
|
+
|
99
|
+
|
100
|
+
def _get_empty_structure(self, target_year, target_month):
|
101
|
+
"""
|
102
|
+
Exception 發生時回傳
|
103
|
+
"""
|
104
|
+
recent_date = [f"{target_year}/{target_month}"]
|
105
|
+
for _ in range(11):
|
106
|
+
target_year, target_month = (target_year - 1, 12) if target_month == 1 else (target_year, target_month - 1)
|
107
|
+
recent_date.append(f"{target_year}/{target_month}")
|
108
|
+
|
109
|
+
def empty_df(index, columns):
|
110
|
+
return pd.DataFrame(index=index, columns=columns)
|
111
|
+
|
112
|
+
return {
|
113
|
+
"month_revenue": empty_df(
|
114
|
+
index=pd.Index(['grand_total', 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], dtype='object', name='month'),
|
115
|
+
columns=pd.Index([f"{target_year - i}" for i in range(10)], dtype=object, name='year')
|
116
|
+
),
|
117
|
+
"this_month_revenue_over_years": empty_df(
|
118
|
+
index=pd.Index(['revenue', 'revenue_increment_ratio', 'YoY_1', 'YoY_3', 'YoY_5', 'YoY_10'], dtype='object'),
|
119
|
+
columns=pd.Index([f"{target_year - i}" for i in range(10)], dtype='int64', name='year')
|
120
|
+
),
|
121
|
+
"grand_total_over_years": empty_df(
|
122
|
+
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'),
|
123
|
+
columns=pd.Index([f"{target_year - i}" for i in range(10)], dtype='int64', name='year')
|
124
|
+
),
|
125
|
+
"recent_month_revenue": empty_df(
|
126
|
+
index=pd.Index(['revenue', 'MoM', 'YoY', 'total_YoY', 'accum_YoY'], dtype='object'),
|
127
|
+
columns=pd.Index([], dtype = 'object', name = 'date')
|
128
|
+
)
|
129
|
+
}
|
@@ -0,0 +1,82 @@
|
|
1
|
+
from ..base import BaseTransformer
|
2
|
+
import pandas as pd
|
3
|
+
|
4
|
+
class BaseProfitLoseTransformer(BaseTransformer):
|
5
|
+
def __init__(self, ticker, company_name, zone):
|
6
|
+
super().__init__(ticker, company_name, zone)
|
7
|
+
|
8
|
+
def _process_twse_to_stats_format(self, fetched_data):
|
9
|
+
table_dict = {}
|
10
|
+
for data in fetched_data:
|
11
|
+
year, season, profit_lose = data['year'], data['season'], data['profit_lose']
|
12
|
+
|
13
|
+
time_index = f"{year}Q{season}"
|
14
|
+
table_dict[time_index] = profit_lose
|
15
|
+
|
16
|
+
stats_df = pd.DataFrame(table_dict)
|
17
|
+
|
18
|
+
return stats_df
|
19
|
+
|
20
|
+
def _process_twse_to_tej_format(self, fetched_data):
|
21
|
+
"""
|
22
|
+
new_df 迎合 TEJ格式, 用於 report_generator
|
23
|
+
"""
|
24
|
+
if (isinstance(fetched_data, list)):
|
25
|
+
table_dict = {}
|
26
|
+
for data in fetched_data:
|
27
|
+
year, season, profit_lose = data['year'], data['season'], data['profit_lose']
|
28
|
+
time_index = f"{year}Q{season}"
|
29
|
+
|
30
|
+
new_profit_lose = self.flatten_dict(
|
31
|
+
profit_lose,
|
32
|
+
target_keys=[
|
33
|
+
"value", "growth",
|
34
|
+
"percentage", "grand_total_percentage"
|
35
|
+
] + [f"YoY_{i}" for i in [1, 3, 5, 10]]
|
36
|
+
+ [f"grand_total_YoY_{i}" for i in [1, 3, 5, 10]]
|
37
|
+
)
|
38
|
+
|
39
|
+
table_dict[time_index] = new_profit_lose
|
40
|
+
|
41
|
+
stats_df = pd.DataFrame.from_dict(table_dict)
|
42
|
+
return stats_df.T
|
43
|
+
|
44
|
+
elif (isinstance(fetched_data, dict)):
|
45
|
+
for time_index, profit_lose in fetched_data.items():
|
46
|
+
fetched_data[time_index] = self.flatten_dict(
|
47
|
+
value_dict=profit_lose,
|
48
|
+
target_keys=[
|
49
|
+
"value", "growth", "percentage", "grand_total",
|
50
|
+
"grand_total_percentage"
|
51
|
+
] + [f"YoY_{i}" for i in [1, 3, 5, 10]] +
|
52
|
+
[f"grand_total_YoY_{i}" for i in [1, 3, 5, 10]]
|
53
|
+
)
|
54
|
+
|
55
|
+
stats_df = pd.DataFrame.from_dict(fetched_data)
|
56
|
+
return stats_df.T
|
57
|
+
|
58
|
+
def _process_us_format(self, fetched_data):
|
59
|
+
"""
|
60
|
+
主要用於report generator
|
61
|
+
"""
|
62
|
+
if (isinstance(fetched_data, list)):
|
63
|
+
table_dict = {}
|
64
|
+
for data in fetched_data:
|
65
|
+
year, season, income_statement = data['year'], data['season'], data['income_statement']
|
66
|
+
time_index = f"{year}Q{season}"
|
67
|
+
|
68
|
+
table_dict[time_index] = income_statement
|
69
|
+
|
70
|
+
stats_df = pd.DataFrame.from_dict(table_dict)
|
71
|
+
return stats_df.T
|
72
|
+
|
73
|
+
elif (isinstance(fetched_data, dict)):
|
74
|
+
for time_index, income_statement in fetched_data.items():
|
75
|
+
fetched_data[time_index] = self.flatten_dict(
|
76
|
+
value_dict=income_statement,
|
77
|
+
target_keys=["value", "growth"] +
|
78
|
+
[f"YoY_{i}" for i in [1, 3, 5, 10]]
|
79
|
+
)
|
80
|
+
stats_df = pd.DataFrame.from_dict(fetched_data)
|
81
|
+
return stats_df.T
|
82
|
+
|
@@ -0,0 +1,133 @@
|
|
1
|
+
from .base import BaseProfitLoseTransformer
|
2
|
+
from neurostats_API.utils import StatsProcessor, YoY_Calculator
|
3
|
+
import pandas as pd
|
4
|
+
|
5
|
+
|
6
|
+
class TWSEProfitLoseTransformer(BaseProfitLoseTransformer):
|
7
|
+
def __init__(self, ticker, company_name, zone):
|
8
|
+
super().__init__(ticker, company_name, zone)
|
9
|
+
|
10
|
+
# 載入欄位切片設定檔(定義哪些欄位要顯示在報表上)
|
11
|
+
self.table_settings = StatsProcessor.load_yaml("twse/profit_lose.yaml")
|
12
|
+
|
13
|
+
# 設定最後回傳的所有欄位 key,包含主頁面、總覽、成長率、比率等
|
14
|
+
self.return_keys = [
|
15
|
+
'profit_lose', 'grand_total_profit_lose', 'revenue', 'grand_total_revenue',
|
16
|
+
'gross_profit', 'grand_total_gross_profit', 'gross_profit_percentage',
|
17
|
+
'grand_total_gross_profit_percentage', 'operating_income', 'grand_total_operating_income', 'operating_income_percentage',
|
18
|
+
'grand_total_operating_income_percentage', 'net_income_before_tax', 'grand_total_net_income_before_tax', 'net_income_before_tax_percentage',
|
19
|
+
'grand_total_net_income_before_tax_percentage', 'net_income', 'grand_total_net_income', 'net_income_percentage',
|
20
|
+
'grand_total_income_percentage', 'EPS', 'EPS_growth', 'grand_total_EPS',
|
21
|
+
'grand_total_EPS_growth', 'profit_lose_all', 'profit_lose_YoY'
|
22
|
+
]
|
23
|
+
|
24
|
+
self.stats_df = None # 主頁面統計用 DataFrame
|
25
|
+
self.new_df = None # 完整表格用 DataFrame(包含 TEJ 格式)
|
26
|
+
|
27
|
+
def process_transform(self, fetched_data):
|
28
|
+
if (not fetched_data):
|
29
|
+
return self._get_empty_structure() # 若查無資料,回傳空表結構
|
30
|
+
|
31
|
+
processed_data = self._process_fn(fetched_data)
|
32
|
+
|
33
|
+
return processed_data
|
34
|
+
|
35
|
+
|
36
|
+
def _process_fn(self, fetched_data):
|
37
|
+
return_dict = {
|
38
|
+
"ticker": self.ticker,
|
39
|
+
"company_name": self.company_name,
|
40
|
+
}
|
41
|
+
|
42
|
+
target_season = fetched_data[-1]['season'] # 用來選擇同一季的欄位當作 YoY 主體
|
43
|
+
|
44
|
+
# 轉換原始 JSON -> dict 格式(key: Q1 ~ Q4)
|
45
|
+
stats_dict = self._process_twse_to_stats_format(fetched_data).to_dict()
|
46
|
+
|
47
|
+
# 計算 QoQ 與 YoY 成長率欄位
|
48
|
+
stats_dict = YoY_Calculator.cal_QoQ(stats_dict)
|
49
|
+
|
50
|
+
# 建立主頁面 DataFrame 並擴充 _value / _percentage 欄位
|
51
|
+
self.stats_df = pd.DataFrame.from_dict(stats_dict)
|
52
|
+
self.stats_df = self._slice_target_season(self.stats_df, target_season)
|
53
|
+
self.stats_df = StatsProcessor.expand_value_percentage(self.stats_df)
|
54
|
+
|
55
|
+
# 處理各種單位轉換欄位
|
56
|
+
self.stats_df = self._apply_process_unit_pipeline(
|
57
|
+
self.stats_df,
|
58
|
+
postfix_list=[
|
59
|
+
'_value',
|
60
|
+
'_percentage',
|
61
|
+
'_growth',
|
62
|
+
"_YoY_1",
|
63
|
+
"_YoY_3",
|
64
|
+
"_YoY_5",
|
65
|
+
"_YoY_10",
|
66
|
+
]
|
67
|
+
)
|
68
|
+
|
69
|
+
# 篩選主頁面欄位(排除 grand_total 欄)
|
70
|
+
grand_total_value_col = self.stats_df.columns.str.endswith("grand_total_value")
|
71
|
+
grand_total_percentage_col = self.stats_df.columns.str.endswith("grand_total_percentage")
|
72
|
+
self.stats_df = self.stats_df.loc[
|
73
|
+
:,
|
74
|
+
(~grand_total_value_col) & (~grand_total_percentage_col)
|
75
|
+
]
|
76
|
+
|
77
|
+
|
78
|
+
stats_main_page_df = StatsProcessor.slice_table(
|
79
|
+
self.stats_df,
|
80
|
+
mode = 'value_and_percentage',
|
81
|
+
)
|
82
|
+
stats_grand_total_df = StatsProcessor.slice_table(
|
83
|
+
self.stats_df,
|
84
|
+
mode = 'grand_total_values'
|
85
|
+
)
|
86
|
+
|
87
|
+
# 建立完整表格 new_df,轉為 TEJ 標準格式並做單位處理
|
88
|
+
self.new_df = self._process_twse_to_tej_format(stats_dict)
|
89
|
+
self.new_df = self._apply_process_unit_pipeline(
|
90
|
+
self.new_df,
|
91
|
+
postfix_list=[
|
92
|
+
'_value',
|
93
|
+
'_percentage',
|
94
|
+
'_growth',
|
95
|
+
"_YoY_1",
|
96
|
+
"_YoY_3",
|
97
|
+
"_YoY_5",
|
98
|
+
"_YoY_10",
|
99
|
+
]
|
100
|
+
)
|
101
|
+
# 建立 YoY 子表格(以 transposed 的方式)
|
102
|
+
new_df_YoY = self._slice_target_season(self.new_df.T, target_season)
|
103
|
+
|
104
|
+
# 回填主欄位資訊至結果 dict
|
105
|
+
return_dict.update({
|
106
|
+
"profit_lose": stats_main_page_df,
|
107
|
+
"grand_total_profit_lose": stats_grand_total_df,
|
108
|
+
"profit_lose_all": self.new_df.T,
|
109
|
+
"profit_lose_YoY": new_df_YoY
|
110
|
+
})
|
111
|
+
|
112
|
+
# 根據設定檔切出其他欄位(如營收、毛利率等)
|
113
|
+
self._process_target_columns(
|
114
|
+
return_dict,
|
115
|
+
new_df_YoY.T,
|
116
|
+
)
|
117
|
+
|
118
|
+
return return_dict
|
119
|
+
|
120
|
+
def _process_target_columns(self, return_dict ,data_df):
|
121
|
+
for name, setting in self.table_settings.items():
|
122
|
+
target_indexes = setting.get('target_index', [None])
|
123
|
+
for target_index in target_indexes:
|
124
|
+
try:
|
125
|
+
# 使用設定檔定義的 mode + index,切出對應欄位表格
|
126
|
+
return_dict[name] = StatsProcessor.slice_table(
|
127
|
+
total_table=data_df,
|
128
|
+
mode=setting['mode'],
|
129
|
+
target_index=target_index
|
130
|
+
)
|
131
|
+
break # 第一個成功就跳出(允許 fallback 多個 index)
|
132
|
+
except Exception as e:
|
133
|
+
continue
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from .base import BaseProfitLoseTransformer
|
2
|
+
from neurostats_API.utils import StatsProcessor, YoY_Calculator
|
3
|
+
import pandas as pd
|
4
|
+
|
5
|
+
class USProfitLoseTransformer(BaseProfitLoseTransformer):
|
6
|
+
def __init__(self, ticker, company_name, zone):
|
7
|
+
super().__init__(ticker, company_name, zone)
|
8
|
+
self.data_df = None
|
9
|
+
|
10
|
+
def process_transform(self, fetched_data):
|
11
|
+
|
12
|
+
data_dict = self._process_us_format(fetched_data).T.to_dict()
|
13
|
+
|
14
|
+
data_dict = YoY_Calculator.cal_QoQ(data_dict)
|
15
|
+
data_dict = YoY_Calculator.cal_YoY(data_dict)
|
16
|
+
|
17
|
+
self.data_df = self._process_us_format(data_dict)
|
18
|
+
|
19
|
+
return_dict = {
|
20
|
+
"ticker": self.ticker,
|
21
|
+
"company_name": self.company_name,
|
22
|
+
"profit_lose": self.data_df
|
23
|
+
}
|
24
|
+
|
25
|
+
return return_dict
|
@@ -0,0 +1 @@
|
|
1
|
+
from .finance_statement import TEJFinanceStatementTransformer
|
@@ -0,0 +1,149 @@
|
|
1
|
+
from ..base import BaseTransformer
|
2
|
+
from neurostats_API.utils import StatsProcessor, YoY_Calculator
|
3
|
+
import pandas as pd
|
4
|
+
|
5
|
+
|
6
|
+
class BaseTEJTransformer(BaseTransformer):
|
7
|
+
|
8
|
+
def __init__(self, ticker, company_name, zone):
|
9
|
+
super().__init__(ticker, company_name, zone)
|
10
|
+
self.thousand_index_list = [] # 在子類別定義
|
11
|
+
self.percent_index_list = [] # 在子類別定義
|
12
|
+
self.skip_index = [] # 在子類別定義
|
13
|
+
|
14
|
+
def _process_data_to_tej_format(self, fetched_data, target_key):
|
15
|
+
table_dict = {}
|
16
|
+
for data in fetched_data:
|
17
|
+
year, season, target_dict = data['year'], data['season'], data[
|
18
|
+
target_key]
|
19
|
+
time_index = f"{year}Q{season}"
|
20
|
+
table_dict[time_index] = target_dict
|
21
|
+
|
22
|
+
return table_dict
|
23
|
+
|
24
|
+
def _transform_value(self, data_dict):
|
25
|
+
"""
|
26
|
+
處理千元, %等單位
|
27
|
+
"""
|
28
|
+
|
29
|
+
data_df = pd.DataFrame.from_dict(data_dict)
|
30
|
+
for category, postfix in [(self.thousand_index_list, "千元"),
|
31
|
+
(self.percent_index_list, "%")]:
|
32
|
+
process_list = list(set(data_df.index) & set(category))
|
33
|
+
|
34
|
+
if postfix == "%":
|
35
|
+
data_df = data_df.T
|
36
|
+
data_df[process_list] = data_df[process_list].map(
|
37
|
+
lambda x: f"{x}%"
|
38
|
+
) # if (not np.isnan(x)) else None)
|
39
|
+
data_df = data_df.T
|
40
|
+
else:
|
41
|
+
data_df.loc[process_list] = data_df.loc[process_list].map(
|
42
|
+
lambda x: StatsProcessor.
|
43
|
+
cal_non_percentage(x, postfix=postfix)
|
44
|
+
)
|
45
|
+
return data_df.to_dict()
|
46
|
+
|
47
|
+
def _calculate_growth(self, this_value, last_value, delta):
|
48
|
+
try:
|
49
|
+
return YoY_Calculator.cal_growth(
|
50
|
+
this_value, last_value, delta
|
51
|
+
) * 100
|
52
|
+
except Exception:
|
53
|
+
return None
|
54
|
+
|
55
|
+
def _get_QoQ_data(self, data, use_cal=False):
|
56
|
+
|
57
|
+
if (use_cal):
|
58
|
+
new_dict = {}
|
59
|
+
for time_index, data_dict in data.items():
|
60
|
+
year, season = time_index.split('Q')
|
61
|
+
year, season = int(year), int(season)
|
62
|
+
last_year, last_season = (year - 1, 4) if season == 1 else (
|
63
|
+
year, season - 1
|
64
|
+
)
|
65
|
+
|
66
|
+
pre_data_dict = data.get(f"{last_year}Q{last_season}", {})
|
67
|
+
|
68
|
+
new_dict[time_index] = self._calculate_QoQ_dict(
|
69
|
+
data_dict, pre_data_dict, delta=1
|
70
|
+
)
|
71
|
+
data = pd.DataFrame.from_dict(new_dict)
|
72
|
+
return data
|
73
|
+
|
74
|
+
else:
|
75
|
+
data = pd.DataFrame.from_dict(data)
|
76
|
+
return data
|
77
|
+
|
78
|
+
def _get_YoY_data(self, data, target_season, use_cal=False):
|
79
|
+
if (use_cal):
|
80
|
+
new_dict = {}
|
81
|
+
year_shifts = [1, 3, 5, 10]
|
82
|
+
|
83
|
+
for time_index, data_dict in data.items():
|
84
|
+
year, season = time_index.split('Q')
|
85
|
+
year, season = int(year), int(season)
|
86
|
+
|
87
|
+
target_items = []
|
88
|
+
for shift in year_shifts:
|
89
|
+
last_year, last_season = (year - shift, season)
|
90
|
+
|
91
|
+
pre_data_dict = data.get(f"{last_year}Q{last_season}", {})
|
92
|
+
|
93
|
+
target_items.append(pre_data_dict)
|
94
|
+
|
95
|
+
new_dict[time_index] = self._calculate_YoY_dict(
|
96
|
+
data_dict, target_items, year_shifts = year_shifts
|
97
|
+
)
|
98
|
+
|
99
|
+
data = pd.DataFrame.from_dict(new_dict)
|
100
|
+
target_season_col = data.columns.str.endswith(f"{target_season}")
|
101
|
+
data = data.loc[:, target_season_col]
|
102
|
+
|
103
|
+
else:
|
104
|
+
data = pd.DataFrame.from_dict(data)
|
105
|
+
target_season_col = data.columns.str.endswith(f"{target_season}")
|
106
|
+
data = data.loc[:, target_season_col]
|
107
|
+
|
108
|
+
return data
|
109
|
+
|
110
|
+
def _calculate_QoQ_dict(self, this_dict, last_dict, delta):
|
111
|
+
temp_dict = {}
|
112
|
+
for key in list(this_dict.keys()):
|
113
|
+
if key in self.skip_index:
|
114
|
+
temp_dict[f"{key}_value"] = this_dict[key]
|
115
|
+
continue
|
116
|
+
this_value = self._process_value(this_dict[key])
|
117
|
+
|
118
|
+
last_value = last_dict.get(key, None)
|
119
|
+
last_value = self._process_value(last_value)
|
120
|
+
|
121
|
+
growth = self._calculate_growth(
|
122
|
+
this_value, last_value, delta=delta
|
123
|
+
) if last_value is not None else None
|
124
|
+
|
125
|
+
temp_dict[f"{key}_value"] = this_dict[key]
|
126
|
+
temp_dict[f"{key}_growth"] = (f"{growth:.2f}%" if growth else None)
|
127
|
+
|
128
|
+
return temp_dict
|
129
|
+
|
130
|
+
def _calculate_YoY_dict(self, this_dict, last_dicts, year_shifts):
|
131
|
+
temp_dict = {}
|
132
|
+
for last_dict, delta in zip(last_dicts, year_shifts):
|
133
|
+
for key in list(this_dict.keys()):
|
134
|
+
if key in self.skip_index:
|
135
|
+
temp_dict[f"{key}_value"] = this_dict[key]
|
136
|
+
continue
|
137
|
+
this_value = self._process_value(this_dict[key])
|
138
|
+
|
139
|
+
last_value = last_dict.get(key, None)
|
140
|
+
last_value = self._process_value(last_value)
|
141
|
+
|
142
|
+
growth = self._calculate_growth(
|
143
|
+
this_value, last_value, delta=delta
|
144
|
+
) if last_value is not None else None
|
145
|
+
|
146
|
+
temp_dict[f"{key}_value"] = this_dict[key]
|
147
|
+
temp_dict[f"{key}_YoY_{delta}"] = (f"{growth:.2f}%" if growth else None)
|
148
|
+
|
149
|
+
return temp_dict
|