neurostats-API 0.0.3__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|