File size: 7,342 Bytes
3e48f76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import datetime
import yfinance as yf
import pandas as pd
import logging
from yfinance.search import Search
import numpy as np

class SP500Sector:
    MATERIALS = "Materials"
    COMMUNICATION_SERVICES = "Communication Services"
    CONSUMER_DISCRETIONARY = "Consumer Discretionary"
    CONSUMER_STAPLES = "Consumer Staples"
    ENERGY = "Energy"
    FINANCIALS = "Financials"
    HEALTH_CARE = "Health Care"
    INDUSTRIALS = "Industrials"
    OTHER = "Taiwan Other"
    REAL_ESTATE = "Real Estate"
    INFORMATION_TECHNOLOGY = "Information Technology"
    UTILITIES = "Utilities"

    @classmethod
    def get_all_sectors(cls):
        return sorted([v for k, v in vars(cls).items() if k.isupper() and isinstance(v, str)])

    @classmethod
    def get_sector_id(cls, sector_str):
        sectors = cls.get_all_sectors()
        if sector_str in sectors:
            return sectors.index(sector_str)
        return sectors.index(cls.OTHER)

    def isMember(self, sector_str):
        return sector_str in self.get_all_sectors()

    def fromYfinanceSector(self, sector_str):
        if sector_str == "Basic Materials":
            return self.MATERIALS
        elif sector_str == "Communication Services":
            return self.COMMUNICATION_SERVICES
        elif sector_str == "Consumer Cyclical":
            return self.CONSUMER_DISCRETIONARY
        elif sector_str == "Consumer Defensive":
            return self.CONSUMER_STAPLES
        elif sector_str == "Energy":
            return self.ENERGY
        elif sector_str == "Financial Services":
            return self.FINANCIALS
        elif sector_str == "Healthcare":
            return self.HEALTH_CARE
        elif sector_str == "Industrials":
            return self.INDUSTRIALS
        elif sector_str == "Real Estate":
            return self.REAL_ESTATE
        elif sector_str == "Technology":
            return self.INFORMATION_TECHNOLOGY
        elif sector_str == "Utilities":
            return self.UTILITIES
        else:
            return self.OTHER

def get_yfinance_ticker_info(code) -> dict:
    """
    Get ticker information using yfinance search method, which is faster than downloading history.
    Returns a dictionary with keys
    """
    if not hasattr(get_yfinance_ticker_info, "call_counter"):
        get_yfinance_ticker_info.call_counter = 0

    get_yfinance_ticker_info.call_counter += 1
    # Suppress yfinance logging to avoid cluttering output
    logger = logging.getLogger('yfinance')
    original_level = logger.level
    logger.setLevel(logging.CRITICAL)
    try:
        call_count = get_yfinance_ticker_info.call_counter
        results = Search(code, max_results=10).quotes
        if call_count == 1:
            print(results[0])
            
        print(f"{get_yfinance_ticker_info.call_counter}. Searched results for '{code}': {[x.get('symbol') for x in results]}")
        for r in results:
            if r.get("symbol") == code:
                return {k: r.get(k, '') for k in ["exchange", "symbol", "longname", "sector"]}
        
        for r in results:
            if r.get("symbol", "").startswith(code):
                return {k: r.get(k, '') for k in ["exchange", "symbol", "longname", "sector"]}
        
        if results:
            r = results[0]
            return {k: r.get(k, '') for k in ["exchange", "symbol", "longname", "sector"]}
        
        return None
            
    except Exception as e:
        print(f"Warning: Search method failed for '{code}' with error: {e}.")
    finally:
        logger.setLevel(original_level)

    print(f"Warning: Could not find valid ticker for {code} using search.")
    return None

def get_ticker_start_date(code):
    """
    Get the start date (first trade date) of a stock without downloading all data.
    """
    try:
        ticker = yf.Ticker(code)
        # get_history_metadata fetches lightweight metadata about the listing
        meta = ticker.get_history_metadata()
        return meta.get("firstTradeDate", None)
    except Exception as e:
        print(f"Warning: Could not fetch start date for '{code}': {e}")
    return None

def calculate_return(df: pd.DataFrame) -> pd.Series:
    return df['Close'].pct_change()

def calculate_log_return(df: pd.DataFrame) -> pd.Series:
    return np.log(df['Close'] / df['Close'].shift(1))

def calculate_ma_5(df: pd.DataFrame) -> pd.Series:
    return df['Close'].rolling(window=5).mean()

def calculate_ma_20(df: pd.DataFrame) -> pd.Series:
    return df['Close'].rolling(window=20).mean()

def calculate_vol_20(df: pd.DataFrame) -> pd.Series:
    return df['Return'].rolling(window=20).std()

def calculate_roc_10(df: pd.DataFrame) -> pd.Series:
    return (df['Close'] - df['Close'].shift(10)) / df['Close'].shift(10)

def calculate_rsi_14(df: pd.DataFrame) -> pd.Series:
    window = 14
    delta = df['Close'].diff()
    gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
    rs = gain / loss
    return 100 - (100 / (1 + rs))

def calculate_macd(df: pd.DataFrame) -> pd.Series:
    ema_12 = df['Close'].ewm(span=12, adjust=False).mean()
    ema_26 = df['Close'].ewm(span=26, adjust=False).mean()
    return ema_12 - ema_26

def calculate_macd_signal(df: pd.DataFrame) -> pd.Series:
    macd = calculate_macd(df)
    return macd.ewm(span=9, adjust=False).mean()

def calculate_kd_k(df: pd.DataFrame, n: int = 9) -> pd.Series:
    if 'High' not in df.columns or 'Low' not in df.columns:
        return pd.Series(50.0, index=df.index)
    low_min = df['Low'].rolling(window=n).min()
    high_max = df['High'].rolling(window=n).max()
    rsv = 100 * (df['Close'] - low_min) / (high_max - low_min + 1e-8)
    # com=2 is exponentially equivalent to alpha=1/3, matching standard KD smoothing
    return rsv.ewm(com=2, adjust=False).mean()

def calculate_kd_d(df: pd.DataFrame) -> pd.Series:
    k = calculate_kd_k(df)
    return k.ewm(com=2, adjust=False).mean()

def calculate_bb_upper(df: pd.DataFrame) -> pd.Series:
    ma_20 = calculate_ma_20(df)
    std_20 = df['Close'].rolling(window=20).std()
    return ma_20 + 2 * std_20

def calculate_bb_lower(df: pd.DataFrame) -> pd.Series:
    ma_20 = calculate_ma_20(df)
    std_20 = df['Close'].rolling(window=20).std()
    return ma_20 - 2 * std_20

def calculate_bias_6(df: pd.DataFrame) -> pd.Series:
    ma_6 = df['Close'].rolling(window=6).mean()
    return (df['Close'] - ma_6) / ma_6 * 100

feature_map = {
    'Return': calculate_return,
    'Log_Return': calculate_log_return,
    'MA_5': calculate_ma_5,
    'MA_20': calculate_ma_20,
    'Vol_20': calculate_vol_20,
    'ROC_10': calculate_roc_10,
    'RSI_14': calculate_rsi_14,
    'MACD': calculate_macd,
    'MACD_Signal': calculate_macd_signal,
    'KD_K': calculate_kd_k,
    'KD_D': calculate_kd_d,
    'BB_Upper': calculate_bb_upper,
    'BB_Lower': calculate_bb_lower,
    'BIAS_6': calculate_bias_6
}

def generate_features(df: pd.DataFrame, sector_str: str = SP500Sector.OTHER, is_training: bool = True) -> pd.DataFrame:
    for feature_name, calc_func in feature_map.items():
        df[feature_name] = calc_func(df)
        
    df['Sector_ID'] = SP500Sector.get_sector_id(sector_str)
    
    if is_training:
        df['Target'] = df['Close'].shift(-1) / df['Close'] - 1.0
        
    return df