neurostats-API 0.0.24__py3-none-any.whl → 0.0.25__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.
@@ -1,8 +1,17 @@
1
1
  from .base import StatsFetcher, StatsDateTime
2
+ from datetime import datetime
2
3
  import json
3
4
  import numpy as np
4
5
  import pandas as pd
5
- from ..utils import StatsDateTime, StatsProcessor
6
+ from pymongo import ASCENDING, DESCENDING
7
+ import pytz
8
+ import holidays
9
+ import warnings
10
+ warnings.filterwarnings(
11
+ "ignore",
12
+ category=holidays.deprecations.v1_incompatibility.FutureIncompatibilityWarning
13
+ )
14
+ from ..utils import StatsDateTime, StatsProcessor, NoCompanyError
6
15
  import importlib.resources as pkg_resources
7
16
  import yaml
8
17
 
@@ -16,9 +25,11 @@ class FinanceOverviewFetcher(StatsFetcher):
16
25
  super().__init__(ticker, db_client)
17
26
 
18
27
  self.target_fields = StatsProcessor.load_yaml(
19
- "twse/finance_overview_dict.yaml")
28
+ "twse/finance_overview_dict.yaml"
29
+ )
20
30
  self.inverse_dict = StatsProcessor.load_txt(
21
- "twse/seasonal_data_field_dict.txt", json_load=True)
31
+ "twse/seasonal_data_field_dict.txt", json_load=True
32
+ )
22
33
 
23
34
  def prepare_query(self, target_year, target_season):
24
35
 
@@ -33,47 +44,56 @@ class FinanceOverviewFetcher(StatsFetcher):
33
44
  try:
34
45
  small_targets = target_sets['field']
35
46
 
36
- value_index = target_sets['value'] # "金額" or "%"
47
+ value_index = target_sets['value'] # "金額" or "%"
37
48
 
38
49
  for small_target in small_targets:
39
50
  big_target = self.inverse_dict[
40
- small_target] # balance_sheet/profit_lose/cash_flow
51
+ small_target] # balance_sheet/profit_lose/cash_flow
41
52
  if (small_target == "利息費用_bank"):
42
53
  small_target = small_target[:small_target.find("_bank")]
43
- target_query.update({
44
- f"{key}":
45
- f"$$target_season_data.{big_target}.{small_target}.{value_index}"
46
- })
54
+ target_query.update(
55
+ {
56
+ f"{key}":
57
+ f"$$target_season_data.{big_target}.{small_target}.{value_index}"
58
+ }
59
+ )
47
60
  except Exception:
48
61
  continue
49
62
 
50
- pipeline.append({
51
- "$project": {
52
- "_id": 0,
53
- "ticker": 1,
54
- "company_name": 1,
55
- "seasonal_data": {
56
- "$map": {
57
- "input": {
58
- "$filter": {
59
- "input": "$seasonal_data",
60
- "as": "season",
61
- "cond": {
62
- "$and": [{
63
- "$eq": ["$$season.year", target_year]
64
- }, {
65
- "$eq":
66
- ["$$season.season", target_season]
67
- }]
63
+ pipeline.append(
64
+ {
65
+ "$project": {
66
+ "_id": 0,
67
+ "ticker": 1,
68
+ "company_name": 1,
69
+ "seasonal_data": {
70
+ "$map": {
71
+ "input": {
72
+ "$filter": {
73
+ "input": "$seasonal_data",
74
+ "as": "season",
75
+ "cond": {
76
+ "$and": [
77
+ {
78
+ "$eq":
79
+ ["$$season.year", target_year]
80
+ }, {
81
+ "$eq": [
82
+ "$$season.season",
83
+ target_season
84
+ ]
85
+ }
86
+ ]
87
+ }
68
88
  }
69
- }
70
- },
71
- "as": "target_season_data",
72
- "in": target_query
89
+ },
90
+ "as": "target_season_data",
91
+ "in": target_query
92
+ }
73
93
  }
74
94
  }
75
95
  }
76
- })
96
+ )
77
97
 
78
98
  return pipeline
79
99
 
@@ -90,7 +110,8 @@ class FinanceOverviewFetcher(StatsFetcher):
90
110
 
91
111
  try:
92
112
  latest_time = StatsDateTime.get_latest_time(
93
- self.ticker, self.collection)['last_update_time']
113
+ self.ticker, self.collection
114
+ )['last_update_time']
94
115
  year = latest_time['seasonal_data']['latest_year']
95
116
  season = latest_time['seasonal_data']['latest_season']
96
117
  except Exception as e:
@@ -107,7 +128,7 @@ class FinanceOverviewFetcher(StatsFetcher):
107
128
  fetched_data['seasonal_data'] = finance_dict
108
129
 
109
130
  return fetched_data
110
-
131
+
111
132
  def fill_nan_index(self, finance_dict):
112
133
  for key in self.target_fields.keys():
113
134
  if (key not in finance_dict.keys()):
@@ -121,68 +142,76 @@ class FinanceOverviewProcessor(StatsProcessor):
121
142
  for key in finance_dict:
122
143
  if ('YoY' in key):
123
144
  finance_dict[key] = StatsProcessor.cal_percentage(
124
- finance_dict[key])
145
+ finance_dict[key]
146
+ )
125
147
  elif ("rate" in key or 'ratio' in key):
126
148
  finance_dict[key] = StatsProcessor.cal_non_percentage(
127
- finance_dict[key], to_str=True, postfix='%')
149
+ finance_dict[key], to_str=True, postfix='%'
150
+ )
128
151
  else:
129
152
  finance_dict[key] = StatsProcessor.cal_non_percentage(
130
- finance_dict[key])
131
-
132
-
153
+ finance_dict[key]
154
+ )
155
+
133
156
  @classmethod
134
157
  def process_thousand_dollar(cls, finance_dict):
135
158
  process_index = [
136
- "revenue",
137
- "gross_profit",
138
- "operating_income",
139
- "net_income",
140
- "operating_cash_flow",
141
- "invest_cash_flow",
142
- "financing_cash_flow",
143
- "fcf",
144
-
145
- 'current_assets',
146
- 'current_liabilities',
147
- 'non_current_assets',
148
- 'non_current_liabilities',
149
- 'total_assets',
150
- "total_liabilities",
151
- "equity"
159
+ "revenue", "gross_profit", "operating_income", "net_income",
160
+ "operating_cash_flow", "invest_cash_flow", "financing_cash_flow",
161
+ "fcf", 'current_assets', 'current_liabilities',
162
+ 'non_current_assets', 'non_current_liabilities', 'total_assets',
163
+ "total_liabilities", "equity"
152
164
  ]
153
-
165
+
154
166
  for index in process_index:
155
167
  try:
156
- finance_dict[index] = StatsProcessor.cal_non_percentage(finance_dict[index], postfix="千元")
168
+ finance_dict[index] = StatsProcessor.cal_non_percentage(
169
+ finance_dict[index], postfix="千元"
170
+ )
157
171
  except Exception as e:
158
172
  finance_dict[index] = None
159
-
160
173
 
161
174
  @classmethod
162
175
  def process_all(cls, finance_dict):
163
176
  methods = [
164
- cls.cal_EBIT, cls.cal_share_outstanding, cls.cal_fcf,
165
- cls.cal_interest_bearing_debt, cls.cal_revenue_per_share,
166
- cls.cal_gross_per_share, cls.cal_operating_income_per_share,
167
- cls.cal_operating_cash_flow_per_share, cls.fcf_per_share,
168
- cls.cal_roa, cls.cal_roe, cls.cal_gross_over_asset, cls.cal_roce,
169
- cls.cal_gross_profit_marginal, cls.cal_operation_profit_rate,
170
- cls.cal_operating_cash_flow_profit_rate, cls.cal_dso,
171
- cls.cal_account_receive_over_revenue, cls.cal_dpo,
172
- cls.cal_inventories_cycle_ratio, cls.cal_dio,
177
+ cls.cal_EBIT,
178
+ cls.cal_share_outstanding,
179
+ cls.cal_fcf,
180
+ cls.cal_interest_bearing_debt,
181
+ cls.cal_revenue_per_share,
182
+ cls.cal_gross_per_share,
183
+ cls.cal_operating_income_per_share,
184
+ cls.cal_operating_cash_flow_per_share,
185
+ cls.fcf_per_share,
186
+ cls.cal_roa,
187
+ cls.cal_roe,
188
+ cls.cal_gross_over_asset,
189
+ cls.cal_roce,
190
+ cls.cal_gross_profit_marginal,
191
+ cls.cal_operation_profit_rate,
192
+ cls.cal_operating_cash_flow_profit_rate,
193
+ cls.cal_dso,
194
+ cls.cal_account_receive_over_revenue,
195
+ cls.cal_dpo,
196
+ cls.cal_inventories_cycle_ratio,
197
+ cls.cal_dio,
173
198
  cls.cal_inventories_revenue_ratio,
174
- cls.cal_cash_of_conversion_cycle, cls.cal_asset_turnover,
175
- cls.cal_application_turnover, cls.cal_current_ratio,
176
- cls.cal_quick_ratio, cls.cal_debt_to_equity_ratio,
177
- cls.cal_net_debt_to_equity_ratio, cls.cal_interest_coverage_ratio,
199
+ cls.cal_cash_of_conversion_cycle,
200
+ cls.cal_asset_turnover,
201
+ cls.cal_application_turnover,
202
+ cls.cal_current_ratio,
203
+ cls.cal_quick_ratio,
204
+ cls.cal_debt_to_equity_ratio,
205
+ cls.cal_net_debt_to_equity_ratio,
206
+ cls.cal_interest_coverage_ratio,
178
207
  cls.cal_debt_to_operating_cash_flow,
179
- cls.cal_debt_to_free_cash_flow, cls.cal_cash_flow_ratio,
208
+ cls.cal_debt_to_free_cash_flow,
209
+ cls.cal_cash_flow_ratio,
180
210
  ]
181
211
 
182
212
  for method in methods:
183
213
  method(finance_dict)
184
214
 
185
-
186
215
  @classmethod
187
216
  def cal_EBIT(cls, finance_dict):
188
217
  """
@@ -190,9 +219,10 @@ class FinanceOverviewProcessor(StatsProcessor):
190
219
  EBIT = 營業收入 - 營業成本 - 營業費用 - 所得稅費用
191
220
  """
192
221
  try:
193
- EBIT = (finance_dict['revenue'] - finance_dict['operating_cost'] -
194
- finance_dict['operating_expenses'] -
195
- finance_dict['tax_fee'])
222
+ EBIT = (
223
+ finance_dict['revenue'] - finance_dict['operating_cost'] -
224
+ finance_dict['operating_expenses'] - finance_dict['tax_fee']
225
+ )
196
226
  finance_dict['EBIT'] = StatsProcessor.cal_non_percentage(EBIT)
197
227
  except (KeyError, ZeroDivisionError, TypeError) as e:
198
228
  finance_dict['EBIT'] = None
@@ -205,8 +235,10 @@ class FinanceOverviewProcessor(StatsProcessor):
205
235
  自由現金流 = 營業現金流 + 投資現金流
206
236
  """
207
237
  try:
208
- fcf = (finance_dict["operating_cash_flow"] +
209
- finance_dict["financing_cash_flow"])
238
+ fcf = (
239
+ finance_dict["operating_cash_flow"] +
240
+ finance_dict["financing_cash_flow"]
241
+ )
210
242
  finance_dict["fcf"] = StatsProcessor.cal_non_percentage(fcf)
211
243
  except (KeyError, ZeroDivisionError, TypeError) as e:
212
244
  finance_dict['fcf'] = None
@@ -238,8 +270,9 @@ class FinanceOverviewProcessor(StatsProcessor):
238
270
  流通股數 = 本期淨利 ÷ 基本每股盈餘
239
271
  """
240
272
  try:
241
- finance_dict["share_outstanding"] = (finance_dict['net_income'] /
242
- finance_dict['eps'])
273
+ finance_dict["share_outstanding"] = (
274
+ finance_dict['net_income'] / finance_dict['eps']
275
+ )
243
276
  except (KeyError, ZeroDivisionError, TypeError) as e:
244
277
  finance_dict['share_outstanding'] = None
245
278
  # print(f"share_outstanding failed because of {str(e)}")
@@ -251,11 +284,13 @@ class FinanceOverviewProcessor(StatsProcessor):
251
284
  每股營收 = 營業收入 / 在外流通股數
252
285
  """
253
286
  try:
254
- revenue_per_share = (finance_dict['revenue'] /
255
- finance_dict['share_outstanding'])
256
- finance_dict[
257
- 'revenue_per_share'] = StatsProcessor.cal_non_percentage(
258
- revenue_per_share, False)
287
+ revenue_per_share = (
288
+ finance_dict['revenue'] / finance_dict['share_outstanding']
289
+ )
290
+ finance_dict['revenue_per_share'
291
+ ] = StatsProcessor.cal_non_percentage(
292
+ revenue_per_share, False
293
+ )
259
294
  except (KeyError, ZeroDivisionError, TypeError) as e:
260
295
  finance_dict['revenue_per_share'] = None
261
296
  # print(f"revenue_per_share failed because of {str(e)}")
@@ -269,17 +304,17 @@ class FinanceOverviewProcessor(StatsProcessor):
269
304
  if ('gross_profit' not in finance_dict.keys()):
270
305
  try:
271
306
  finance_dict['gross_profit'] = (
272
- finance_dict['revenue'] -
273
- finance_dict['operating_cost']
307
+ finance_dict['revenue'] - finance_dict['operating_cost']
274
308
  )
275
309
  except:
276
310
  finance_dict['gross_profit'] = None
277
311
  try:
278
- gross_per_share = (finance_dict['gross_profit'] /
279
- finance_dict['share_outstanding'])
280
- finance_dict[
281
- 'gross_per_share'] = StatsProcessor.cal_non_percentage(
282
- gross_per_share, False)
312
+ gross_per_share = (
313
+ finance_dict['gross_profit'] / finance_dict['share_outstanding']
314
+ )
315
+ finance_dict['gross_per_share'] = StatsProcessor.cal_non_percentage(
316
+ gross_per_share, False
317
+ )
283
318
 
284
319
  except (KeyError, ZeroDivisionError, TypeError) as e:
285
320
  finance_dict['gross_per_share'] = None
@@ -292,11 +327,14 @@ class FinanceOverviewProcessor(StatsProcessor):
292
327
  每股營業利益= (當期營業利益)÷(當期在外流通股數)
293
328
  """
294
329
  try:
295
- operating_income_per_share = (finance_dict['operating_income'] /
296
- finance_dict['share_outstanding'])
297
- finance_dict[
298
- 'operating_income_per_share'] = StatsProcessor.cal_non_percentage(
299
- operating_income_per_share)
330
+ operating_income_per_share = (
331
+ finance_dict['operating_income'] /
332
+ finance_dict['share_outstanding']
333
+ )
334
+ finance_dict['operating_income_per_share'
335
+ ] = StatsProcessor.cal_non_percentage(
336
+ operating_income_per_share
337
+ )
300
338
  except (KeyError, ZeroDivisionError, TypeError) as e:
301
339
  finance_dict['operating_income_per_share'] = None
302
340
  # print(f"operating_income_per_share failed because of {str(e)}")
@@ -310,10 +348,12 @@ class FinanceOverviewProcessor(StatsProcessor):
310
348
  try:
311
349
  operating_cash_flow_per_share = (
312
350
  finance_dict["operating_cash_flow"] /
313
- finance_dict['share_outstanding'])
314
- finance_dict[
315
- "operating_cash_flow_per_share"] = StatsProcessor.cal_non_percentage(
316
- operating_cash_flow_per_share)
351
+ finance_dict['share_outstanding']
352
+ )
353
+ finance_dict["operating_cash_flow_per_share"
354
+ ] = StatsProcessor.cal_non_percentage(
355
+ operating_cash_flow_per_share
356
+ )
317
357
  except (KeyError, ZeroDivisionError, TypeError) as e:
318
358
  finance_dict['operating_cash_flow_per_share'] = None
319
359
  # print(f'operating_cash_flow_per_share because of {str(e)}')
@@ -325,10 +365,12 @@ class FinanceOverviewProcessor(StatsProcessor):
325
365
  每股自由現金流 = (當期自由現金流) ÷(當期在外流通股數)
326
366
  """
327
367
  try:
328
- fcf_per_share = (finance_dict['fcf'] /
329
- finance_dict['share_outstanding'])
368
+ fcf_per_share = (
369
+ finance_dict['fcf'] / finance_dict['share_outstanding']
370
+ )
330
371
  finance_dict['fcf_per_share'] = StatsProcessor.cal_non_percentage(
331
- fcf_per_share)
372
+ fcf_per_share
373
+ )
332
374
  except (KeyError, ZeroDivisionError, TypeError) as e:
333
375
  finance_dict['fcf_per_share'] = None
334
376
  # print(f"fcf_per_share failed because of {str(e)}")
@@ -344,7 +386,7 @@ class FinanceOverviewProcessor(StatsProcessor):
344
386
  try:
345
387
  roa = (
346
388
  finance_dict['net_income'] + finance_dict['interest'] +
347
- (1 * 0.1) # 有效稅率需要改,這裡先設0.1
389
+ (1 * 0.1) # 有效稅率需要改,這裡先設0.1
348
390
  ) / finance_dict['inventories']
349
391
 
350
392
  finance_dict["roa"] = StatsProcessor.cal_percentage(roa)
@@ -369,10 +411,12 @@ class FinanceOverviewProcessor(StatsProcessor):
369
411
  計算營業毛利/總資產
370
412
  """
371
413
  try:
372
- gross_over_asset = (finance_dict['gross_profit'] /
373
- finance_dict['total_assets'])
414
+ gross_over_asset = (
415
+ finance_dict['gross_profit'] / finance_dict['total_assets']
416
+ )
374
417
  finance_dict['gross_over_asset'] = StatsProcessor.cal_percentage(
375
- gross_over_asset)
418
+ gross_over_asset
419
+ )
376
420
  except (KeyError, ZeroDivisionError, TypeError) as e:
377
421
  finance_dict['gross_over_asset'] = None
378
422
  # print(f"營業毛利/總資產 failed because of {str(e)}")
@@ -384,10 +428,15 @@ class FinanceOverviewProcessor(StatsProcessor):
384
428
  ROCE = (稅前淨利+利息費用) / (資產總額-流動負債)
385
429
  """
386
430
  try:
387
- roce = ((finance_dict['net_income_before_tax'] +
388
- finance_dict['interest']) /
389
- (finance_dict['total_assets'] -
390
- finance_dict['current_liabilities']))
431
+ roce = (
432
+ (
433
+ finance_dict['net_income_before_tax'] +
434
+ finance_dict['interest']
435
+ ) / (
436
+ finance_dict['total_assets'] -
437
+ finance_dict['current_liabilities']
438
+ )
439
+ )
391
440
  finance_dict['roce'] = StatsProcessor.cal_percentage(roce)
392
441
 
393
442
  except (KeyError, ZeroDivisionError, TypeError) as e:
@@ -401,11 +450,12 @@ class FinanceOverviewProcessor(StatsProcessor):
401
450
  營業毛利率 = 營業毛利 ÷ 營業收入
402
451
  """
403
452
  try:
404
- gross_profit_margin = (finance_dict['gross_profit'] /
405
- finance_dict['revenue'])
406
- finance_dict[
407
- 'gross_profit_margin'] = StatsProcessor.cal_percentage(
408
- gross_profit_margin)
453
+ gross_profit_margin = (
454
+ finance_dict['gross_profit'] / finance_dict['revenue']
455
+ )
456
+ finance_dict['gross_profit_margin'] = StatsProcessor.cal_percentage(
457
+ gross_profit_margin
458
+ )
409
459
  except Exception as e:
410
460
  finance_dict['gross_profit_margin'] = None
411
461
  # print(f"gross_profit_margin failed because of {str(e)}")
@@ -419,10 +469,12 @@ class FinanceOverviewProcessor(StatsProcessor):
419
469
  try:
420
470
  operation_profit_rate = (
421
471
  finance_dict['revenue'] - finance_dict['operating_cost'] -
422
- finance_dict['operating_expenses']) / finance_dict['revenue']
423
- finance_dict[
424
- "operation_profit_rate"] = StatsProcessor.cal_percentage(
425
- operation_profit_rate)
472
+ finance_dict['operating_expenses']
473
+ ) / finance_dict['revenue']
474
+ finance_dict["operation_profit_rate"
475
+ ] = StatsProcessor.cal_percentage(
476
+ operation_profit_rate
477
+ )
426
478
  except (KeyError, ZeroDivisionError, TypeError) as e:
427
479
  finance_dict["operation_profit_rate"] = None
428
480
  # print(f"operation_profit failed because of {str(e)}")
@@ -435,10 +487,12 @@ class FinanceOverviewProcessor(StatsProcessor):
435
487
  """
436
488
  try:
437
489
  operating_cash_flow_profit_rate = (
438
- finance_dict["operating_cash_flow"] / finance_dict["revenue"])
439
- finance_dict[
440
- "operating_cash_flow_profit_rate"] = StatsProcessor.cal_percentage(
441
- operating_cash_flow_profit_rate)
490
+ finance_dict["operating_cash_flow"] / finance_dict["revenue"]
491
+ )
492
+ finance_dict["operating_cash_flow_profit_rate"
493
+ ] = StatsProcessor.cal_percentage(
494
+ operating_cash_flow_profit_rate
495
+ )
442
496
  except (KeyError, ZeroDivisionError, TypeError) as e:
443
497
  finance_dict["operating_cash_flow_profit_rate"] = None
444
498
  # print(
@@ -460,10 +514,12 @@ class FinanceOverviewProcessor(StatsProcessor):
460
514
  DSO = 365 × (應收帳款平均餘額 ÷ 營業收入)
461
515
  """
462
516
  try:
463
- dso = (365 *
464
- (finance_dict['account_pay'] / finance_dict['revenue']))
517
+ dso = (
518
+ 365 * (finance_dict['account_pay'] / finance_dict['revenue'])
519
+ )
465
520
  finance_dict['dso'] = StatsProcessor.cal_non_percentage(
466
- dso, to_str=True, postfix="日")
521
+ dso, to_str=True, postfix="日"
522
+ )
467
523
  except Exception as e:
468
524
  finance_dict['dso'] = None
469
525
  # print(f"Error calculating 應收帳款收現天數 because of {str(e)}")
@@ -475,14 +531,15 @@ class FinanceOverviewProcessor(StatsProcessor):
475
531
  = 應收帳款平均餘額 ÷ 營業收入
476
532
  """
477
533
  try:
478
- account_receive_over_revenue = (finance_dict['account_receive'] /
479
- finance_dict['revenue'])
480
- finance_dict[
481
- "account_receive_over_revenue"] = StatsProcessor.cal_percentage(
482
- account_receive_over_revenue)
534
+ account_receive_over_revenue = (
535
+ finance_dict['account_receive'] / finance_dict['revenue']
536
+ )
537
+ finance_dict["account_receive_over_revenue"
538
+ ] = StatsProcessor.cal_percentage(
539
+ account_receive_over_revenue
540
+ )
483
541
  except Exception as e:
484
- finance_dict[
485
- "account_receive_over_revenue"] = None
542
+ finance_dict["account_receive_over_revenue"] = None
486
543
 
487
544
  @classmethod
488
545
  def cal_dpo(cls, finance_dict):
@@ -493,9 +550,11 @@ class FinanceOverviewProcessor(StatsProcessor):
493
550
  try:
494
551
  dpo = (
495
552
  365 *
496
- (finance_dict['account_pay'] / finance_dict['operating_cost']))
553
+ (finance_dict['account_pay'] / finance_dict['operating_cost'])
554
+ )
497
555
  finance_dict["dpo"] = StatsProcessor.cal_non_percentage(
498
- dpo, to_str=True, postfix="日")
556
+ dpo, to_str=True, postfix="日"
557
+ )
499
558
  except (KeyError, ZeroDivisionError, TypeError) as e:
500
559
  finance_dict["dpo"] = None
501
560
  # print(f"應付帳款週轉天數 failed because of {str(e)}")
@@ -507,12 +566,14 @@ class FinanceOverviewProcessor(StatsProcessor):
507
566
  = 銷貨成本 ÷ 存貨
508
567
  """
509
568
  try:
510
- inventories_cycle_ratio = (finance_dict['operating_cost'] /
511
- finance_dict['inventories'])
512
-
513
- finance_dict[
514
- "inventories_cycle_ratio"] = StatsProcessor.cal_percentage(
515
- inventories_cycle_ratio)
569
+ inventories_cycle_ratio = (
570
+ finance_dict['operating_cost'] / finance_dict['inventories']
571
+ )
572
+
573
+ finance_dict["inventories_cycle_ratio"
574
+ ] = StatsProcessor.cal_percentage(
575
+ inventories_cycle_ratio
576
+ )
516
577
  except (KeyError, ZeroDivisionError, TypeError) as e:
517
578
  finance_dict["inventories_cycle_ratio"] = None
518
579
  # print(f"Error calculating 存貨周轉率 because of {str(e)}")
@@ -525,10 +586,12 @@ class FinanceOverviewProcessor(StatsProcessor):
525
586
  MUDA MUDA MUDA !!!
526
587
  """
527
588
  try:
528
- dio = 365 * (finance_dict["inventories"] /
529
- finance_dict["operating_cost"])
589
+ dio = 365 * (
590
+ finance_dict["inventories"] / finance_dict["operating_cost"]
591
+ )
530
592
  finance_dict["dio"] = StatsProcessor.cal_non_percentage(
531
- dio, to_str=True, postfix="日")
593
+ dio, to_str=True, postfix="日"
594
+ )
532
595
  except (KeyError, ZeroDivisionError, TypeError) as e:
533
596
  finance_dict["dio"] = None
534
597
  # print(f"Error calculating 存貨週轉天數 because of {str(e)}")
@@ -540,12 +603,14 @@ class FinanceOverviewProcessor(StatsProcessor):
540
603
  存貨佔營收比= 存貨 ÷ 營業收入
541
604
  """
542
605
  try:
543
- inventories_revenue_ratio = (finance_dict['inventories'] /
544
- finance_dict['revenue'])
545
-
546
- finance_dict[
547
- "inventories_revenue_ratio"] = StatsProcessor.cal_percentage(
548
- inventories_revenue_ratio)
606
+ inventories_revenue_ratio = (
607
+ finance_dict['inventories'] / finance_dict['revenue']
608
+ )
609
+
610
+ finance_dict["inventories_revenue_ratio"
611
+ ] = StatsProcessor.cal_percentage(
612
+ inventories_revenue_ratio
613
+ )
549
614
  except (KeyError, ZeroDivisionError, TypeError) as e:
550
615
  finance_dict["inventories_revenue_ratio"] = None
551
616
  # print(f"Error calculating 存貨佔營收比率 because of {str(e)}")
@@ -557,12 +622,13 @@ class FinanceOverviewProcessor(StatsProcessor):
557
622
  存貨週轉天數 + 應收帳款週轉天數 - 應付帳款週轉天數
558
623
  """
559
624
  try:
560
- cash_of_conversion_cycle = (finance_dict["dio"] +
561
- finance_dict["dso"] -
562
- finance_dict['dpo'])
563
- finance_dict[
564
- "cash_of_conversion_cycle"] = StatsProcessor.cal_non_percentage(
565
- cash_of_conversion_cycle, to_str=True, postfix="日")
625
+ cash_of_conversion_cycle = (
626
+ finance_dict["dio"] + finance_dict["dso"] - finance_dict['dpo']
627
+ )
628
+ finance_dict["cash_of_conversion_cycle"
629
+ ] = StatsProcessor.cal_non_percentage(
630
+ cash_of_conversion_cycle, to_str=True, postfix="日"
631
+ )
566
632
  except (KeyError, ZeroDivisionError, TypeError) as e:
567
633
  finance_dict["cash_of_conversion_cycle"] = None
568
634
 
@@ -573,10 +639,12 @@ class FinanceOverviewProcessor(StatsProcessor):
573
639
  營業收入 ÷ 資產總額
574
640
  """
575
641
  try:
576
- asset_turnover = (finance_dict["revenue"] /
577
- finance_dict["inventories"])
642
+ asset_turnover = (
643
+ finance_dict["revenue"] / finance_dict["inventories"]
644
+ )
578
645
  finance_dict["asset_turnover"] = StatsProcessor.cal_percentage(
579
- asset_turnover)
646
+ asset_turnover
647
+ )
580
648
  except Exception as e:
581
649
  finance_dict["asset_turnover"] = None
582
650
 
@@ -587,15 +655,14 @@ class FinanceOverviewProcessor(StatsProcessor):
587
655
  營業收入 ÷ 不動產、廠房與設備平均餘額
588
656
  """
589
657
  try:
590
- applcation_turnover = (finance_dict['revenue'] /
591
- finance_dict["application"])
592
- finance_dict[
593
- 'application_turnover'] = StatsProcessor.cal_percentage(
594
- applcation_turnover)
658
+ applcation_turnover = (
659
+ finance_dict['revenue'] / finance_dict["application"]
660
+ )
661
+ finance_dict['application_turnover'
662
+ ] = StatsProcessor.cal_percentage(applcation_turnover)
595
663
 
596
664
  except Exception as e:
597
665
  finance_dict['application_turnover'] = None
598
-
599
666
 
600
667
  @classmethod
601
668
  def cal_current_ratio(cls, finance_dict):
@@ -603,10 +670,13 @@ class FinanceOverviewProcessor(StatsProcessor):
603
670
  計算流動比率 = 流動資產 / 流動負債
604
671
  """
605
672
  try:
606
- current_ratio = (finance_dict['current_assets'] /
607
- finance_dict['current_liabilities'])
673
+ current_ratio = (
674
+ finance_dict['current_assets'] /
675
+ finance_dict['current_liabilities']
676
+ )
608
677
  finance_dict['current_ratio'] = StatsProcessor.cal_percentage(
609
- current_ratio)
678
+ current_ratio
679
+ )
610
680
  except (KeyError, ZeroDivisionError, TypeError) as e:
611
681
  finance_dict['current_ratio'] = None
612
682
  # print(f"Error calculating current ratio: {e}")
@@ -618,11 +688,12 @@ class FinanceOverviewProcessor(StatsProcessor):
618
688
  (流動資產 - 存貨) / 流動負債
619
689
  """
620
690
  try:
621
- quick_ratio = (finance_dict['current_assets'] -
622
- finance_dict['inventories']
623
- ) / finance_dict['current_liabilities']
691
+ quick_ratio = (
692
+ finance_dict['current_assets'] - finance_dict['inventories']
693
+ ) / finance_dict['current_liabilities']
624
694
  finance_dict['quick_ratio'] = StatsProcessor.cal_percentage(
625
- quick_ratio)
695
+ quick_ratio
696
+ )
626
697
  except (KeyError, ZeroDivisionError, TypeError) as e:
627
698
  finance_dict['quick_ratio'] = None
628
699
  # print(f"Error calculating quick ratio: {e}")
@@ -633,11 +704,12 @@ class FinanceOverviewProcessor(StatsProcessor):
633
704
  # 負債權益比率 = 總負債 / 股東權益
634
705
  """
635
706
  try:
636
- debt_to_equity_ratio = finance_dict[
637
- 'total_liabilities'] / finance_dict['equity']
638
- finance_dict[
639
- 'debt_to_equity_ratio'] = StatsProcessor.cal_percentage(
640
- debt_to_equity_ratio)
707
+ debt_to_equity_ratio = finance_dict['total_liabilities'
708
+ ] / finance_dict['equity']
709
+ finance_dict['debt_to_equity_ratio'
710
+ ] = StatsProcessor.cal_percentage(
711
+ debt_to_equity_ratio
712
+ )
641
713
  except (KeyError, ZeroDivisionError, TypeError) as e:
642
714
  finance_dict['debt_to_equity_ratio'] = None
643
715
  # print(f"Error calculating debt to equity ratio: {e}")
@@ -652,9 +724,10 @@ class FinanceOverviewProcessor(StatsProcessor):
652
724
  finance_dict['total_liabilities'] -
653
725
  finance_dict['cash_and_cash_equivalents']
654
726
  ) / finance_dict['equity']
655
- finance_dict[
656
- 'net_debt_to_equity_ratio'] = StatsProcessor.cal_percentage(
657
- net_debt_to_equity_ratio)
727
+ finance_dict['net_debt_to_equity_ratio'
728
+ ] = StatsProcessor.cal_percentage(
729
+ net_debt_to_equity_ratio
730
+ )
658
731
  except (KeyError, ZeroDivisionError, TypeError) as e:
659
732
  finance_dict['net_debt_to_equity_ratio'] = None
660
733
  # print(f"Error calculating net debt to equity ratio: {e}")
@@ -667,9 +740,10 @@ class FinanceOverviewProcessor(StatsProcessor):
667
740
  try:
668
741
  interest_coverage_ratio = finance_dict['EBIT'] / finance_dict[
669
742
  'interest_expense']
670
- finance_dict[
671
- 'interest_coverage_ratio'] = StatsProcessor.cal_non_percentage(
672
- interest_coverage_ratio, to_str=True, postfix="倍")
743
+ finance_dict['interest_coverage_ratio'
744
+ ] = StatsProcessor.cal_non_percentage(
745
+ interest_coverage_ratio, to_str=True, postfix="倍"
746
+ )
673
747
  except (KeyError, ZeroDivisionError, TypeError) as e:
674
748
  finance_dict['interest_coverage_ratio'] = None
675
749
  # print(f"Error calculating interest coverage ratio: {e}")
@@ -682,9 +756,10 @@ class FinanceOverviewProcessor(StatsProcessor):
682
756
  try:
683
757
  debt_to_operating_cash_flow = finance_dict[
684
758
  'interest_bearing_debt'] / finance_dict['operating_cash_flow']
685
- finance_dict[
686
- 'debt_to_operating_cash_flow'] = StatsProcessor.cal_percentage(
687
- debt_to_operating_cash_flow)
759
+ finance_dict['debt_to_operating_cash_flow'
760
+ ] = StatsProcessor.cal_percentage(
761
+ debt_to_operating_cash_flow
762
+ )
688
763
  except (KeyError, ZeroDivisionError, TypeError) as e:
689
764
  finance_dict['debt_to_operating_cash_flow'] = None
690
765
  # print(f"Error calculating debt to operating cash flow: {e}")
@@ -695,11 +770,12 @@ class FinanceOverviewProcessor(StatsProcessor):
695
770
  # 有息負債 / 自由現金流
696
771
  """
697
772
  try:
698
- debt_to_free_cash_flow = finance_dict[
699
- 'interest_bearing_debt'] / finance_dict['fcf']
700
- finance_dict[
701
- 'debt_to_free_cash_flow'] = StatsProcessor.cal_percentage(
702
- debt_to_free_cash_flow)
773
+ debt_to_free_cash_flow = finance_dict['interest_bearing_debt'
774
+ ] / finance_dict['fcf']
775
+ finance_dict['debt_to_free_cash_flow'
776
+ ] = StatsProcessor.cal_percentage(
777
+ debt_to_free_cash_flow
778
+ )
703
779
  except (KeyError, ZeroDivisionError, TypeError) as e:
704
780
  finance_dict['debt_to_free_cash_flow'] = None
705
781
  # print(f"Error calculating debt to free cash flow: {e}")
@@ -713,7 +789,250 @@ class FinanceOverviewProcessor(StatsProcessor):
713
789
  cash_flow_ratio = finance_dict[
714
790
  'operating_cash_flow'] / finance_dict['current_liabilities']
715
791
  finance_dict['cash_flow_ratio'] = StatsProcessor.cal_percentage(
716
- cash_flow_ratio)
792
+ cash_flow_ratio
793
+ )
717
794
  except (KeyError, ZeroDivisionError, TypeError) as e:
718
795
  finance_dict['cash_flow_ratio'] = None
719
796
  # print(f"Error calculating cash flow ratio: {e}")
797
+
798
+
799
+ class AgentFinanceOverviewFetcher(FinanceOverviewFetcher):
800
+ """
801
+ 目前僅適用美股
802
+ """
803
+
804
+ def __init__(self, ticker, db_client):
805
+ self.ticker = ticker
806
+ self.timezone = pytz.timezone("Asia/Taipei")
807
+ self.tw_company_list = StatsProcessor.load_json("company_list/tw.json")
808
+ self.us_company_list = StatsProcessor.load_json("company_list/us_TradingView_list.json")
809
+
810
+ if (ticker not in self.us_company_list.keys()):
811
+ raise NoCompanyError("class \"AgentFinanceOverviewFetcher\" only supports US company now")
812
+
813
+ db_name = "company" if self.ticker in self.tw_company_list else "company_us"
814
+ self.db = db_client[db_name]
815
+
816
+ daily_collection_name_map = {
817
+ "company": "twse_daily_share_price",
818
+ 'company_us': "us_technical"
819
+ }
820
+
821
+ seasonal_collection_name_map = {
822
+ 'company': "twse_seasonal_report",
823
+ "company_us": "us_fundamentals"
824
+ }
825
+
826
+ self.daily_collection_name = daily_collection_name_map.get(db_name)
827
+ self.seasonal_collection_name = seasonal_collection_name_map.get(
828
+ db_name
829
+ )
830
+
831
+ assert self.daily_collection_name != "unknown", f"請確認 {ticker} 是否是 {','.join(list(daily_collection_name_map.values()))}"
832
+ assert self.seasonal_collection_name != "unknown", f"請確認 {ticker} 是否是 {','.join(list(seasonal_collection_name_map.values()))}"
833
+ self.daily_collection = db_client[db_name][self.daily_collection_name]
834
+ self.seasonal_collection = db_client[db_name][
835
+ self.seasonal_collection_name]
836
+
837
+ def query_data(self, date=None):
838
+
839
+ query_set = self._prepare_query(date)
840
+ if (date is None):
841
+ str_date = datetime.today().strftime("%Y-%m-%d")
842
+ if (isinstance(date, datetime)):
843
+ str_date = date.strftime("%Y-%m-%d")
844
+ elif (isinstance(date, str)):
845
+ str_date = date
846
+
847
+ daily_query, daily_projection, daily_sorting = query_set['daily']
848
+ seasonal_query, seasonal_projection, seasonal_sorting = query_set[
849
+ 'seasonal']
850
+
851
+ daily_datas = self.daily_collection.find(daily_query, daily_projection
852
+ ).sort(daily_sorting)
853
+ seasonal_datas = self.seasonal_collection.find(
854
+ seasonal_query, seasonal_projection
855
+ ).sort(seasonal_sorting)
856
+
857
+ daily_data = [data for data in daily_datas]
858
+ seasonal_data = [
859
+ {
860
+ **data['balance_sheet'],
861
+ **data['income_statement'],
862
+ **data['cash_flow']
863
+ } for data in seasonal_datas
864
+ ]
865
+
866
+ seasonal_df = pd.DataFrame(seasonal_data)
867
+ TTM_data = seasonal_df.mean()
868
+
869
+ return_dict = {
870
+ "date": str_date,
871
+ "ticker": self.ticker,
872
+ "company_name": self._get_company_name(),
873
+ "category": self._get_category(),
874
+ "market_open": self._is_us_market_open(),
875
+ "latest_close":
876
+ self._get_latest_close(daily_data),
877
+ "close_offset":
878
+ self._get_latest_offset(daily_data).get("value", "0.0"),
879
+ "close_offset_percentage":
880
+ self._get_latest_offset(daily_data).get("percentage", "0.0%"),
881
+ "latest_volume":
882
+ self._get_latest_volume(daily_data),
883
+ "average_volume":
884
+ self._get_latest_average_volume(daily_data, 30),
885
+ "market_capitalzation":
886
+ self._get_market_capitalization(daily_data, seasonal_data),
887
+ "P_E":
888
+ self._get_PE(daily_data, TTM_data),
889
+ "P_S":
890
+ self._get_PS(daily_data, TTM_data, seasonal_data),
891
+ }
892
+
893
+ return return_dict
894
+
895
+ def _prepare_query(self, date=None):
896
+ if (date is None):
897
+ date = datetime.today()
898
+ elif (isinstance(date, str)):
899
+ date = datetime.strptime(date, "%Y-%m-%d")
900
+
901
+ daily_query = {"ticker": self.ticker, "date": {"$lte": date}}
902
+
903
+ seasons = []
904
+ year = date.year
905
+ season = (date.month - 1) // 3 + 1
906
+
907
+ for _ in range(4):
908
+ seasons.append({"year": year, "season": season})
909
+ year = year if season != 1 else year - 1
910
+ season = season - 1 if season != 1 else 4
911
+
912
+ seasonal_query = {"ticker": self.ticker, "$or": seasons}
913
+
914
+ daily_projection = {"_id": 0}
915
+ seasonal_projection = {"_id": 0}
916
+
917
+ daily_sorting = {"date": ASCENDING}
918
+ seasonal_sorting = {"year": ASCENDING, "season": ASCENDING}
919
+
920
+ query_set = {
921
+ "daily": [daily_query, daily_projection, daily_sorting],
922
+ "seasonal": [seasonal_query, seasonal_projection, seasonal_sorting]
923
+ }
924
+
925
+ return query_set
926
+
927
+ def _get_latest_close(self, daily_data):
928
+ latest_close = daily_data[-1]['close']
929
+ latest_close = str(round(latest_close, 2))
930
+ return latest_close
931
+
932
+ def _get_latest_offset(self, daily_data):
933
+ latest_close = daily_data[-1]['close']
934
+ previous = daily_data[-2]['close']
935
+
936
+ offset = latest_close - previous
937
+ percentage = offset / previous
938
+
939
+ offset = str(np.round(offset, 2))
940
+
941
+ return {'value': offset, "percentage": f"{percentage:.2f}%"}
942
+
943
+ def _get_latest_volume(self, daily_data):
944
+ try:
945
+ volume = round(daily_data[-1]['volume'], 2)
946
+ return str(volume)
947
+ except Exception as e:
948
+ return "Error"
949
+
950
+ def _get_latest_average_volume(self, daily_data, length=30):
951
+ try:
952
+ daily_data = pd.DataFrame(daily_data).tail(length)
953
+
954
+ value = daily_data['volume'].mean().item()
955
+ value = round(value, 2)
956
+
957
+ return str(value)
958
+
959
+ except Exception as e:
960
+ return "Error"
961
+
962
+ def _get_market_capitalization(self, daily_data, seasonal_data):
963
+ try:
964
+ latest_close = float(self._get_latest_close(daily_data))
965
+ latest_common_share = seasonal_data[-1][
966
+ 'Common Stock']
967
+
968
+ percentage = round((latest_close * latest_common_share), 2)
969
+
970
+ return str(percentage)
971
+
972
+ except Exception as e:
973
+ return "Error"
974
+
975
+ def _get_PE(self, daily_data, TTM_data):
976
+ try:
977
+ latest_close = float(self._get_latest_close(daily_data))
978
+ TTM_EPS = TTM_data['Diluted EPS']
979
+
980
+ value = round((latest_close / TTM_EPS), 2)
981
+
982
+ return str(value)
983
+
984
+ except Exception as e:
985
+ return "Error"
986
+
987
+ def _get_PS(self, daily_data, TTM_data, seasonal_data):
988
+ try:
989
+ market_capitalzation = self._get_market_capitalization(daily_data, seasonal_data)
990
+ market_capitalzation = float(market_capitalzation)
991
+ TTM_revenue = TTM_data['Total Revenue']
992
+
993
+ value = round((market_capitalzation / TTM_revenue), 2)
994
+
995
+ return str(value)
996
+ except Exception as e:
997
+ return "Error"
998
+
999
+ def _is_us_market_open(self):
1000
+
1001
+ taiwan_dt = datetime.now(pytz.timezone('Asia/Taipei'))
1002
+
1003
+ # 轉成美東時間(會自動處理夏令時間)
1004
+ eastern = pytz.timezone('US/Eastern')
1005
+ us_dt = taiwan_dt.astimezone(eastern)
1006
+
1007
+ # 假日判斷
1008
+ us_holidays = holidays.NYSE(years=us_dt.year)
1009
+ if us_dt.date() in us_holidays:
1010
+ return False
1011
+
1012
+ # 週末
1013
+ if us_dt.weekday() >= 5: # 5 = Saturday, 6 = Sunday
1014
+ return False
1015
+
1016
+ # 判斷是否在開盤時間
1017
+ market_open = us_dt.replace(hour=9, minute=30, second=0, microsecond=0)
1018
+ market_close = us_dt.replace(hour=16, minute=0, second=0, microsecond=0)
1019
+
1020
+ return market_open <= us_dt <= market_close
1021
+
1022
+ def _get_category(self):
1023
+ category = self.us_company_list.get(self.ticker,{}).get('en_industry')
1024
+
1025
+ if (category):
1026
+ return category
1027
+
1028
+ else:
1029
+ raise ValueError("No company")
1030
+
1031
+ def _get_company_name(self):
1032
+ company_name = self.us_company_list.get(self.ticker,{}).get('name')
1033
+
1034
+ if (company_name):
1035
+ return company_name
1036
+
1037
+ else:
1038
+ raise ValueError("No company")