neurostats-API 0.0.25rc1__py3-none-any.whl → 1.0.0rc2__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.
Files changed (105) hide show
  1. neurostats_API/__init__.py +1 -1
  2. neurostats_API/async_mode/__init__.py +13 -0
  3. neurostats_API/async_mode/db/__init__.py +3 -0
  4. neurostats_API/async_mode/db/base.py +24 -0
  5. neurostats_API/async_mode/db/tej.py +10 -0
  6. neurostats_API/async_mode/db/twse.py +8 -0
  7. neurostats_API/async_mode/db/us.py +9 -0
  8. neurostats_API/async_mode/db_extractors/__init__.py +20 -0
  9. neurostats_API/async_mode/db_extractors/base.py +66 -0
  10. neurostats_API/async_mode/db_extractors/daily/__init__.py +7 -0
  11. neurostats_API/async_mode/db_extractors/daily/base.py +89 -0
  12. neurostats_API/async_mode/db_extractors/daily/tej_chip.py +14 -0
  13. neurostats_API/async_mode/db_extractors/daily/tej_tech.py +12 -0
  14. neurostats_API/async_mode/db_extractors/daily/twse_chip.py +49 -0
  15. neurostats_API/async_mode/db_extractors/daily/value.py +93 -0
  16. neurostats_API/async_mode/db_extractors/daily/yf.py +12 -0
  17. neurostats_API/async_mode/db_extractors/month_revenue/__init__.py +1 -0
  18. neurostats_API/async_mode/db_extractors/month_revenue/base.py +140 -0
  19. neurostats_API/async_mode/db_extractors/month_revenue/twse.py +5 -0
  20. neurostats_API/async_mode/db_extractors/seasonal/__init__.py +4 -0
  21. neurostats_API/async_mode/db_extractors/seasonal/balance_sheet.py +19 -0
  22. neurostats_API/async_mode/db_extractors/seasonal/base.py +152 -0
  23. neurostats_API/async_mode/db_extractors/seasonal/cashflow.py +10 -0
  24. neurostats_API/async_mode/db_extractors/seasonal/profit_lose.py +17 -0
  25. neurostats_API/async_mode/db_extractors/seasonal/tej.py +87 -0
  26. neurostats_API/async_mode/factory/__init__.py +1 -0
  27. neurostats_API/async_mode/factory/extractor_factory.py +168 -0
  28. neurostats_API/async_mode/factory/transformer_factory.py +164 -0
  29. neurostats_API/async_mode/fetchers/__init__.py +10 -0
  30. neurostats_API/async_mode/fetchers/balance_sheet.py +31 -0
  31. neurostats_API/async_mode/fetchers/base.py +48 -0
  32. neurostats_API/async_mode/fetchers/cash_flow.py +56 -0
  33. neurostats_API/async_mode/fetchers/finance_overview.py +134 -0
  34. neurostats_API/async_mode/fetchers/month_revenue.py +35 -0
  35. neurostats_API/async_mode/fetchers/profit_lose.py +46 -0
  36. neurostats_API/async_mode/fetchers/tech.py +205 -0
  37. neurostats_API/async_mode/fetchers/tej.py +88 -0
  38. neurostats_API/async_mode/fetchers/twse_institution.py +62 -0
  39. neurostats_API/async_mode/fetchers/twse_margin.py +100 -0
  40. neurostats_API/async_mode/fetchers/value.py +76 -0
  41. neurostats_API/config/company_list/ticker_index_industry_map.json +7946 -0
  42. neurostats_API/config/company_list/us.json +9986 -0
  43. neurostats_API/{tools → config}/tej_db/tej_db_skip_index.yaml +0 -2
  44. neurostats_API/{tools → config}/twse/profit_lose.yaml +0 -6
  45. neurostats_API/fetchers/finance_overview.py +27 -5
  46. neurostats_API/transformers/__init__.py +40 -0
  47. neurostats_API/transformers/balance_sheet/__init__.py +2 -0
  48. neurostats_API/transformers/balance_sheet/base.py +51 -0
  49. neurostats_API/transformers/balance_sheet/twse.py +76 -0
  50. neurostats_API/transformers/balance_sheet/us.py +30 -0
  51. neurostats_API/transformers/base.py +110 -0
  52. neurostats_API/transformers/cash_flow/__init__.py +2 -0
  53. neurostats_API/transformers/cash_flow/base.py +114 -0
  54. neurostats_API/transformers/cash_flow/twse.py +68 -0
  55. neurostats_API/transformers/cash_flow/us.py +38 -0
  56. neurostats_API/transformers/daily_chip/__init__.py +1 -0
  57. neurostats_API/transformers/daily_chip/base.py +5 -0
  58. neurostats_API/transformers/daily_chip/tej.py +0 -0
  59. neurostats_API/transformers/daily_chip/twse_chip.py +415 -0
  60. neurostats_API/transformers/daily_chip/utils/__init__.py +0 -0
  61. neurostats_API/transformers/daily_chip/utils/institution.py +90 -0
  62. neurostats_API/transformers/daily_chip/utils/margin_trading.py +2 -0
  63. neurostats_API/transformers/daily_chip/utils/security_lending.py +0 -0
  64. neurostats_API/transformers/daily_tech/__init__.py +1 -0
  65. neurostats_API/transformers/daily_tech/base.py +5 -0
  66. neurostats_API/transformers/daily_tech/tech.py +84 -0
  67. neurostats_API/transformers/daily_tech/utils/__init__.py +1 -0
  68. neurostats_API/transformers/daily_tech/utils/processor.py +251 -0
  69. neurostats_API/transformers/finance_overview/__init__.py +2 -0
  70. neurostats_API/transformers/finance_overview/agent_overview.py +55 -0
  71. neurostats_API/transformers/finance_overview/base.py +824 -0
  72. neurostats_API/transformers/finance_overview/stats_overview.py +64 -0
  73. neurostats_API/transformers/month_revenue/__init__.py +1 -0
  74. neurostats_API/transformers/month_revenue/base.py +60 -0
  75. neurostats_API/transformers/month_revenue/twse.py +129 -0
  76. neurostats_API/transformers/profit_lose/__init__.py +2 -0
  77. neurostats_API/transformers/profit_lose/base.py +82 -0
  78. neurostats_API/transformers/profit_lose/twse.py +133 -0
  79. neurostats_API/transformers/profit_lose/us.py +25 -0
  80. neurostats_API/transformers/tej/__init__.py +1 -0
  81. neurostats_API/transformers/tej/base.py +149 -0
  82. neurostats_API/transformers/tej/finance_statement.py +80 -0
  83. neurostats_API/transformers/value/__init__.py +1 -0
  84. neurostats_API/transformers/value/base.py +5 -0
  85. neurostats_API/transformers/value/tej.py +8 -0
  86. neurostats_API/transformers/value/twse.py +48 -0
  87. neurostats_API/utils/__init__.py +1 -1
  88. neurostats_API/utils/data_process.py +10 -6
  89. neurostats_API/utils/exception.py +8 -0
  90. neurostats_API/utils/logger.py +21 -0
  91. neurostats_API-1.0.0rc2.dist-info/METADATA +102 -0
  92. neurostats_API-1.0.0rc2.dist-info/RECORD +119 -0
  93. neurostats_API-0.0.25rc1.dist-info/METADATA +0 -858
  94. neurostats_API-0.0.25rc1.dist-info/RECORD +0 -36
  95. /neurostats_API/{tools → config}/company_list/tw.json +0 -0
  96. /neurostats_API/{tools → config}/company_list/us_TradingView_list.json +0 -0
  97. /neurostats_API/{tools → config}/tej_db/tej_db_index.yaml +0 -0
  98. /neurostats_API/{tools → config}/tej_db/tej_db_percent_index.yaml +0 -0
  99. /neurostats_API/{tools → config}/tej_db/tej_db_thousand_index.yaml +0 -0
  100. /neurostats_API/{tools → config}/twse/balance_sheet.yaml +0 -0
  101. /neurostats_API/{tools → config}/twse/cash_flow_percentage.yaml +0 -0
  102. /neurostats_API/{tools → config}/twse/finance_overview_dict.yaml +0 -0
  103. /neurostats_API/{tools → config}/twse/seasonal_data_field_dict.txt +0 -0
  104. {neurostats_API-0.0.25rc1.dist-info → neurostats_API-1.0.0rc2.dist-info}/WHEEL +0 -0
  105. {neurostats_API-0.0.25rc1.dist-info → neurostats_API-1.0.0rc2.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,2 @@
1
+ from .twse import TWSEProfitLoseTransformer
2
+ from .us import USProfitLoseTransformer
@@ -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