neurostats-API 0.0.3__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 -0
- neurostats_API/cli.py +35 -0
- neurostats_API/fetchers/__init__.py +1 -0
- neurostats_API/fetchers/balance_sheet.py +0 -0
- neurostats_API/fetchers/base.py +59 -0
- neurostats_API/fetchers/finance_overview.py +501 -0
- neurostats_API/fetchers/month_revenue.py +0 -0
- neurostats_API/fetchers/profit_lose.py +0 -0
- neurostats_API/fetchers/tech.py +312 -0
- neurostats_API/fetchers/value_invest.py +89 -0
- neurostats_API/main.py +27 -0
- neurostats_API/utils/__init__.py +2 -0
- neurostats_API/utils/datetime.py +21 -0
- neurostats_API/utils/db_client.py +10 -0
- neurostats_API/utils/fetcher.py +1137 -0
- neurostats_API-0.0.3.dist-info/METADATA +440 -0
- neurostats_API-0.0.3.dist-info/RECORD +19 -0
- neurostats_API-0.0.3.dist-info/WHEEL +5 -0
- neurostats_API-0.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,312 @@
|
|
1
|
+
from .base import StatsFetcher
|
2
|
+
import pandas as pd
|
3
|
+
|
4
|
+
class TechFetcher(StatsFetcher):
|
5
|
+
|
6
|
+
def __init__(self, ticker:str):
|
7
|
+
super().__init__()
|
8
|
+
self.ticker = ticker
|
9
|
+
self.full_ohlcv = self._get_ohlcv()
|
10
|
+
self.basic_indexes = ['SMA5', 'SMA20', 'SMA60', 'EMA5', 'EMA20',
|
11
|
+
'EMA40', 'EMA12', 'EMA26', 'RSI7', 'RSI14',
|
12
|
+
'RSI21', 'MACD', 'Signal Line', 'Middle Band',
|
13
|
+
'Upper Band', 'Lower Band', '%b', 'ATR',
|
14
|
+
'BBW','EMA Cycle','EMA Cycle Instructions',
|
15
|
+
'Day Trading Signal']
|
16
|
+
|
17
|
+
self.daily_index = TechProcessor.cal_basic_index(self.full_ohlcv)
|
18
|
+
|
19
|
+
self.weekly_index = TechProcessor.resample(
|
20
|
+
self.daily_index,
|
21
|
+
period= 'W',
|
22
|
+
technical_indicators = self.basic_indexes
|
23
|
+
)
|
24
|
+
|
25
|
+
self.monthly_index = TechProcessor.resample(
|
26
|
+
self.daily_index,
|
27
|
+
period= 'ME',
|
28
|
+
technical_indicators = self.basic_indexes
|
29
|
+
)
|
30
|
+
|
31
|
+
self.quarterly_index = TechProcessor.resample(
|
32
|
+
self.daily_index,
|
33
|
+
period= 'QE',
|
34
|
+
technical_indicators = self.basic_indexes
|
35
|
+
)
|
36
|
+
|
37
|
+
self.yearly_index = TechProcessor.resample(
|
38
|
+
self.daily_index,
|
39
|
+
period= 'YE',
|
40
|
+
technical_indicators = self.basic_indexes
|
41
|
+
)
|
42
|
+
|
43
|
+
def _get_ohlcv(self):
|
44
|
+
query = {'ticker': self.ticker}
|
45
|
+
ticker_full = list(self.collection.find(query))
|
46
|
+
|
47
|
+
if not ticker_full:
|
48
|
+
raise ValueError(f"No data found for ticker: {self.ticker}")
|
49
|
+
|
50
|
+
if 'daily_data' not in ticker_full[0] or ticker_full[0]['daily_data'] is None:
|
51
|
+
raise KeyError("Missing 'daily_data' in the retrieved data")
|
52
|
+
|
53
|
+
df = pd.DataFrame(ticker_full[0]['daily_data'])
|
54
|
+
|
55
|
+
selected_cols = ['date','open','high','low','close','volume']
|
56
|
+
|
57
|
+
return df[selected_cols]
|
58
|
+
|
59
|
+
def get_daily(self):
|
60
|
+
|
61
|
+
return self.daily_index
|
62
|
+
|
63
|
+
def get_weekly(self):
|
64
|
+
|
65
|
+
return self.weekly_index
|
66
|
+
|
67
|
+
def get_monthly(self):
|
68
|
+
|
69
|
+
return self.monthly_index
|
70
|
+
|
71
|
+
def get_quarterly(self):
|
72
|
+
|
73
|
+
return self.quarterly_index
|
74
|
+
|
75
|
+
def get_yearly(self):
|
76
|
+
|
77
|
+
return self.yearly_index
|
78
|
+
|
79
|
+
class TechProcessor:
|
80
|
+
|
81
|
+
@staticmethod
|
82
|
+
def cal_sma(closes: pd.Series, n_days: int) -> pd.Series:
|
83
|
+
return closes.rolling(window=n_days).mean()
|
84
|
+
|
85
|
+
@staticmethod
|
86
|
+
def cal_ema(closes: pd.Series, n_days: int) -> pd.Series:
|
87
|
+
return closes.ewm(span=n_days, adjust=False).mean()
|
88
|
+
|
89
|
+
@staticmethod
|
90
|
+
def cal_rsi(closes: pd.Series, n_days: int) -> pd.Series:
|
91
|
+
delta = closes.diff(1)
|
92
|
+
gain = (delta.where(delta > 0, 0)).rolling(window=n_days).mean()
|
93
|
+
loss = (-delta.where(delta < 0, 0)).rolling(window=n_days).mean()
|
94
|
+
rs = gain / loss
|
95
|
+
return 100 - (100 / (1 + rs))
|
96
|
+
|
97
|
+
@staticmethod
|
98
|
+
def cal_macd(ema12s: pd.Series, ema26s: pd.Series) -> pd.Series:
|
99
|
+
return ema12s - ema26s
|
100
|
+
|
101
|
+
@staticmethod
|
102
|
+
def cal_single_line(macds: pd.Series, n_days: int = 9) -> pd.Series:
|
103
|
+
return macds.ewm(span=n_days, adjust=False).mean()
|
104
|
+
|
105
|
+
@staticmethod
|
106
|
+
def cal_bollinger_bands(closes: pd.Series, n_days: int = 20) -> pd.DataFrame:
|
107
|
+
middle = closes.rolling(window=n_days).mean()
|
108
|
+
upper = middle + 2 * closes.rolling(window=n_days).std()
|
109
|
+
lower = middle - 2 * closes.rolling(window=n_days).std()
|
110
|
+
percent_b = (closes - lower) / (upper - lower)
|
111
|
+
|
112
|
+
return pd.DataFrame(
|
113
|
+
{
|
114
|
+
'middle': middle,
|
115
|
+
'upper': upper,
|
116
|
+
"lower": lower,
|
117
|
+
"%b": percent_b
|
118
|
+
}
|
119
|
+
)
|
120
|
+
|
121
|
+
@staticmethod
|
122
|
+
def cal_atr(highes: pd.Series, lows: pd.Series, closes: pd.Series, n_days: int) -> pd.Series:
|
123
|
+
high_low = highes - lows
|
124
|
+
high_close = (highes - closes.shift(1)).abs()
|
125
|
+
low_close = (lows - closes.shift(1)).abs()
|
126
|
+
|
127
|
+
true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
|
128
|
+
atr = true_range.rolling(window=n_days, min_periods=1).mean()
|
129
|
+
|
130
|
+
return atr
|
131
|
+
|
132
|
+
@staticmethod
|
133
|
+
def check_tech_trend(ema5: float, ema20: float, ema40: float) -> str:
|
134
|
+
if ema5 > ema20 > ema40:
|
135
|
+
return '穩定上升期'
|
136
|
+
elif ema20 > ema5 > ema40:
|
137
|
+
return '牛市結束期'
|
138
|
+
elif ema20 > ema40 > ema5:
|
139
|
+
return '熊市入口期'
|
140
|
+
elif ema40 > ema20 > ema5:
|
141
|
+
return '穩定下跌期'
|
142
|
+
elif ema40 > ema5 > ema20:
|
143
|
+
return '熊市結束期'
|
144
|
+
elif ema5 > ema40 > ema20:
|
145
|
+
return '牛市入口期'
|
146
|
+
else:
|
147
|
+
return '未定義'
|
148
|
+
|
149
|
+
@staticmethod
|
150
|
+
def check_day_trading(
|
151
|
+
close_today: float,
|
152
|
+
close_yesterday: float,
|
153
|
+
today_atr: float,
|
154
|
+
today_rsi7: float,
|
155
|
+
target_value=270,
|
156
|
+
atr_thred=7.5
|
157
|
+
) -> str:
|
158
|
+
reasons = []
|
159
|
+
|
160
|
+
# 檢查規則 1: Close 今日 - Close 昨日
|
161
|
+
if close_today - close_yesterday <= 0:
|
162
|
+
reasons.append('今日收盤價未高於昨日收盤價')
|
163
|
+
|
164
|
+
# 檢查規則 2: 今日 Close 是否大於目標值
|
165
|
+
if close_today < target_value:
|
166
|
+
reasons.append(f'今日收盤價未達到目標值{target_value}')
|
167
|
+
|
168
|
+
# 檢查規則 3: ATR 是否大於 atr_thred
|
169
|
+
if today_atr < atr_thred:
|
170
|
+
reasons.append(f'ATR 值小於 {atr_thred}')
|
171
|
+
|
172
|
+
# 檢查規則 4: RSI7 是否大於等於 40
|
173
|
+
if today_rsi7 < 40:
|
174
|
+
reasons.append('RSI7 值小於 40')
|
175
|
+
|
176
|
+
# 根據檢查結果返回
|
177
|
+
if not reasons:
|
178
|
+
return '今日此股票為好的當沖標的'
|
179
|
+
else:
|
180
|
+
return f'今日此股票並非好的當沖標的, 原因: {", ".join(reasons)}'
|
181
|
+
|
182
|
+
@staticmethod
|
183
|
+
def cal_basic_index(ohlcvs:pd.DataFrame):
|
184
|
+
|
185
|
+
# SMA
|
186
|
+
ohlcvs['SMA5'] = TechProcessor.cal_sma(ohlcvs['close'], 5)
|
187
|
+
ohlcvs['SMA20'] = TechProcessor.cal_sma(ohlcvs['close'], 20)
|
188
|
+
ohlcvs['SMA60'] = TechProcessor.cal_sma(ohlcvs['close'], 40)
|
189
|
+
|
190
|
+
# EMA
|
191
|
+
ohlcvs['EMA5'] = TechProcessor.cal_ema(ohlcvs['close'], 5)
|
192
|
+
ohlcvs['EMA20'] = TechProcessor.cal_ema(ohlcvs['close'], 20)
|
193
|
+
ohlcvs['EMA40'] = TechProcessor.cal_ema(ohlcvs['close'], 40)
|
194
|
+
|
195
|
+
ohlcvs['EMA12'] = TechProcessor.cal_ema(ohlcvs['close'], 12)
|
196
|
+
ohlcvs['EMA26'] = TechProcessor.cal_ema(ohlcvs['close'], 26)
|
197
|
+
|
198
|
+
# RSI
|
199
|
+
ohlcvs['RSI7'] = TechProcessor.cal_rsi(ohlcvs['close'], 7)
|
200
|
+
ohlcvs['RSI14'] = TechProcessor.cal_rsi(ohlcvs['close'], 14)
|
201
|
+
ohlcvs['RSI21'] = TechProcessor.cal_rsi(ohlcvs['close'], 21)
|
202
|
+
|
203
|
+
# MACD
|
204
|
+
ohlcvs['MACD'] = TechProcessor.cal_macd(ohlcvs['EMA12'], ohlcvs['EMA26'])
|
205
|
+
ohlcvs['Signal Line'] = TechProcessor.cal_single_line(ohlcvs['MACD'], 9)
|
206
|
+
|
207
|
+
# BANDS
|
208
|
+
bands = TechProcessor.cal_bollinger_bands(ohlcvs['close'], 20)
|
209
|
+
ohlcvs['Middle Band'] = bands['middle']
|
210
|
+
ohlcvs['Upper Band'] = bands['upper']
|
211
|
+
ohlcvs['Lower Band'] = bands['lower']
|
212
|
+
ohlcvs['%b'] = bands['%b']
|
213
|
+
ohlcvs['BBW'] = (ohlcvs["Upper Band"] - ohlcvs["Lower Band"]) / ohlcvs["Middle Band"]
|
214
|
+
|
215
|
+
# ATR
|
216
|
+
ohlcvs['ATR'] = TechProcessor.cal_atr(ohlcvs['high'],ohlcvs['low'],ohlcvs['close'],14)
|
217
|
+
|
218
|
+
# EMA CYCLE
|
219
|
+
ohlcvs['EMA Cycle'] = ohlcvs.apply(
|
220
|
+
lambda row: TechProcessor.check_tech_trend(row['EMA5'], row['EMA20'], row['EMA40']),
|
221
|
+
axis=1
|
222
|
+
)
|
223
|
+
guidance_map = {
|
224
|
+
'穩定上升期': "三條移動平均線都左下右上, 買方優勢, 三線間隔越來越遠時, 進一步強攻",
|
225
|
+
'牛市結束期': "ema20 & 40 左下右上, ema5 緩慢下滑, 行情仍強, 賣出條件為 ema5 持續下跌, ema20 停止上漲",
|
226
|
+
'熊市入口期': "全數出清穩定上升期布局的多頭部位, 考慮提早佈局建立空頭部位",
|
227
|
+
'穩定下跌期': "三條移動平均線都是左上右下, 賣方優勢, 三線間隔越來越遠時, 進一步強攻",
|
228
|
+
'熊市結束期': "ema20 & 40 左上右下, ema5 緩慢上升, 行情仍走弱, 布局買進的條件是 ema 持續上漲, ema20 停止下降, 幾乎持平",
|
229
|
+
'牛市入口期': "全數出清穩定下跌期布局的空頭部位, 考慮提早佈局多頭部位",
|
230
|
+
'未定義': "無對應指導"
|
231
|
+
}
|
232
|
+
|
233
|
+
ohlcvs['EMA Cycle Instructions'] = ohlcvs['EMA Cycle'].map(guidance_map)
|
234
|
+
|
235
|
+
# DAY TRADE SELECTING
|
236
|
+
ohlcvs['close_yesterday'] = ohlcvs['close'].shift(1)
|
237
|
+
ohlcvs['Day Trading Signal'] = ohlcvs.apply(
|
238
|
+
lambda row: TechProcessor.check_day_trading(
|
239
|
+
close_today=row['close'],
|
240
|
+
close_yesterday=row['close_yesterday'], # 使用前一天的收盤價
|
241
|
+
today_atr=row['ATR'],
|
242
|
+
today_rsi7=row['RSI7']
|
243
|
+
),
|
244
|
+
axis=1
|
245
|
+
)
|
246
|
+
|
247
|
+
return ohlcvs
|
248
|
+
|
249
|
+
@staticmethod
|
250
|
+
def resample(df: pd.DataFrame, period='W', technical_indicators=None, date_col='date'):
|
251
|
+
"""
|
252
|
+
將 DataFrame 中的技術指標數據重新取樣為指定的時間週期。
|
253
|
+
參數:
|
254
|
+
df (pd.DataFrame): 包含技術指標的日線 DataFrame,必須包含 datetime 索引或指定的日期列。
|
255
|
+
period (str): 重新取樣的時間週期,如 'W', 'ME', 'QE', 'YE'。
|
256
|
+
technical_indicators (list): 需要重新取樣的技術指標列名。
|
257
|
+
date_col (str): 包含日期的列名(如果不是索引)。
|
258
|
+
|
259
|
+
返回:
|
260
|
+
pd.DataFrame: 重新取樣後的 DataFrame。
|
261
|
+
"""
|
262
|
+
# 標記輸入的日期是否是索引還是列
|
263
|
+
original_index = df.index.name == date_col if df.index.name else False
|
264
|
+
|
265
|
+
# 檢查是否需要將日期列設置為索引
|
266
|
+
if not original_index and date_col in df.columns:
|
267
|
+
df[date_col] = pd.to_datetime(df[date_col])
|
268
|
+
df.set_index(date_col, inplace=True)
|
269
|
+
|
270
|
+
# 過濾掉非數字型的列
|
271
|
+
numeric_df = df.select_dtypes(include='number')
|
272
|
+
|
273
|
+
# 預設的聚合方式
|
274
|
+
agg_dict = {
|
275
|
+
'open': 'first',
|
276
|
+
'high': 'max',
|
277
|
+
'low': 'min',
|
278
|
+
'close': 'last',
|
279
|
+
'volume': 'sum'
|
280
|
+
}
|
281
|
+
|
282
|
+
# 如果沒有提供 technical_indicators,設置默認技術指標
|
283
|
+
if technical_indicators is None:
|
284
|
+
technical_indicators = numeric_df.columns.tolist()
|
285
|
+
|
286
|
+
# 將技術指標的聚合方式設置為 'mean'
|
287
|
+
for indicator in technical_indicators:
|
288
|
+
if indicator in numeric_df.columns:
|
289
|
+
agg_dict[indicator] = 'mean'
|
290
|
+
|
291
|
+
# 過濾出存在於 DataFrame 中的列
|
292
|
+
existing_cols = {col: agg_dict[col] for col in agg_dict if col in numeric_df.columns}
|
293
|
+
|
294
|
+
# 確保索引為 DatetimeIndex,進行重新取樣
|
295
|
+
if not isinstance(df.index, pd.DatetimeIndex):
|
296
|
+
raise TypeError("The DataFrame index must be a DatetimeIndex for resampling.")
|
297
|
+
|
298
|
+
resampled_df = numeric_df.resample(period).agg(existing_cols)
|
299
|
+
|
300
|
+
# 如果原始日期是列而非索引,將日期從索引還原為列並重置索引為範圍索引
|
301
|
+
if not original_index:
|
302
|
+
resampled_df.reset_index(inplace=True)
|
303
|
+
|
304
|
+
return resampled_df
|
305
|
+
|
306
|
+
|
307
|
+
|
308
|
+
|
309
|
+
|
310
|
+
|
311
|
+
|
312
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
from .base import StatsFetcher, StatsProcessor
|
2
|
+
from datetime import datetime, timedelta, date
|
3
|
+
import pandas as pd
|
4
|
+
from utils import StatsDateTime
|
5
|
+
|
6
|
+
|
7
|
+
class ValueFetcher(StatsFetcher):
|
8
|
+
|
9
|
+
def __init__(self, ticker: str):
|
10
|
+
super().__init__(ticker)
|
11
|
+
|
12
|
+
def prepare_query(self, start_date, end_date):
|
13
|
+
pipeline = super().prepare_query()
|
14
|
+
|
15
|
+
pipeline.append({
|
16
|
+
"$project": {
|
17
|
+
"_id": 0,
|
18
|
+
"ticker": 1,
|
19
|
+
"company_name": 1,
|
20
|
+
"daily_data": {
|
21
|
+
"$map": {
|
22
|
+
"input": {
|
23
|
+
"$filter": {
|
24
|
+
"input": "$daily_data",
|
25
|
+
"as": "daily",
|
26
|
+
"cond": {
|
27
|
+
"$and": [{
|
28
|
+
"$gte": ["$$daily.date", start_date]
|
29
|
+
}, {
|
30
|
+
"$lte": ["$$daily.date", end_date]
|
31
|
+
}]
|
32
|
+
}
|
33
|
+
}
|
34
|
+
},
|
35
|
+
"as": "daily_item",
|
36
|
+
"in": {
|
37
|
+
"date": "$$daily_item.date",
|
38
|
+
"close": "$$daily_item.close",
|
39
|
+
"P_B": "$$daily_item.P_B",
|
40
|
+
"P_E": "$$daily_item.P_E",
|
41
|
+
"P_FCF": "$$daily_item.P_FCF",
|
42
|
+
"P_S": "$$daily_item.P_S",
|
43
|
+
"EV_OPI": "$$daily_item.EV_OPI",
|
44
|
+
"EV_EBIT": "$$daily_item.EV_EBIT",
|
45
|
+
"EV_EBITDA": "$$daily_item.EV_EBITDA",
|
46
|
+
"EV_S": "$$daily_item.EV_S"
|
47
|
+
}
|
48
|
+
}
|
49
|
+
},
|
50
|
+
"yearly_data": 1
|
51
|
+
}
|
52
|
+
})
|
53
|
+
|
54
|
+
return pipeline
|
55
|
+
|
56
|
+
def query_data(self):
|
57
|
+
today = StatsDateTime.get_today()
|
58
|
+
|
59
|
+
this_year = today.year - 1911
|
60
|
+
start_date = (today.date - timedelta(days=31))
|
61
|
+
end_date = today.date
|
62
|
+
|
63
|
+
fetched_data = self.collect_data(start_date, end_date)
|
64
|
+
|
65
|
+
fetched_data['daily_data'] = fetched_data['daily_data'][-1]
|
66
|
+
|
67
|
+
fetched_data['yearly_data'] = ValueProcessor.transform_to_df(
|
68
|
+
fetched_data['daily_data'],
|
69
|
+
fetched_data['yearly_data'],
|
70
|
+
)
|
71
|
+
|
72
|
+
return fetched_data
|
73
|
+
|
74
|
+
|
75
|
+
class ValueProcessor(StatsProcessor):
|
76
|
+
|
77
|
+
@staticmethod
|
78
|
+
def transform_to_df(daily_dict, yearly_dict):
|
79
|
+
latest_data = {"year": f"過去4季"}
|
80
|
+
|
81
|
+
latest_data.update(daily_dict)
|
82
|
+
latest_data.pop("date")
|
83
|
+
latest_data.pop("close")
|
84
|
+
|
85
|
+
yearly_dict.append(latest_data)
|
86
|
+
|
87
|
+
yearly_dict = pd.DataFrame.from_dict(yearly_dict)
|
88
|
+
|
89
|
+
return yearly_dict
|
neurostats_API/main.py
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
import pandas as pd
|
2
|
+
|
3
|
+
from utils import StatsFetcher
|
4
|
+
|
5
|
+
pd.options.display.max_rows = 4
|
6
|
+
pd.options.display.max_columns = 4
|
7
|
+
|
8
|
+
if __name__ == "__main__":
|
9
|
+
import pprint
|
10
|
+
pp = pprint.PrettyPrinter(indent=2)
|
11
|
+
|
12
|
+
ticker = "2330"
|
13
|
+
start_date = "2018-01-01"
|
14
|
+
end_date = "2199-12-31"
|
15
|
+
|
16
|
+
fetcher = StatsFetcher()
|
17
|
+
|
18
|
+
# pp.pprint(fetcher.query_seasonal_data(ticker, start_date, end_date, 'balance_sheet'))
|
19
|
+
# fetcher.get_balance_sheet(ticker)
|
20
|
+
query_data = fetcher.get_profit_lose(ticker)
|
21
|
+
print("{")
|
22
|
+
for key, value in query_data.items():
|
23
|
+
print(f"\t\"{key}\":")
|
24
|
+
|
25
|
+
print(f"{value}")
|
26
|
+
print("}")
|
27
|
+
# pp.pprint(fetcher.get_balance_sheet(ticker))
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
|
3
|
+
class StatsDateTime():
|
4
|
+
|
5
|
+
def __init__(self, date, year, month, day, season):
|
6
|
+
self.date = date
|
7
|
+
self.year = year
|
8
|
+
self.month = month
|
9
|
+
self.day = day
|
10
|
+
self.season = season
|
11
|
+
|
12
|
+
@classmethod
|
13
|
+
def get_today(cls):
|
14
|
+
today = datetime.today()
|
15
|
+
this_year = today.year
|
16
|
+
this_month = today.month
|
17
|
+
this_day = today.day
|
18
|
+
this_season = ((this_month - 1) // 3) + 1
|
19
|
+
|
20
|
+
return StatsDateTime(today, this_year, this_month, this_day,
|
21
|
+
this_season)
|