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.
@@ -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,2 @@
1
+ from .fetcher import StatsFetcher
2
+ from .datetime import StatsDateTime
@@ -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)
@@ -0,0 +1,10 @@
1
+ import os
2
+ from pymongo import MongoClient
3
+ from dotenv import load_dotenv
4
+
5
+ # 載入 .env 檔案
6
+ load_dotenv()
7
+
8
+ # 創建並共享 MongoClient 連線
9
+ mongo_uri = os.getenv('MONGO_URI', 'mongodb://localhost:27017')
10
+ shared_client = MongoClient(mongo_uri)