quantmine 0.2.0__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.
- quantmine/__init__.py +97 -0
- quantmine/back_testing.py +381 -0
- quantmine/config.py +115 -0
- quantmine/data_acquisition.py +326 -0
- quantmine/datareader.py +119 -0
- quantmine/factor_attribution.py +70 -0
- quantmine/factor_mining.py +192 -0
- quantmine/factor_register.py +73 -0
- quantmine/ic_calculator.py +543 -0
- quantmine/load_config.py +16 -0
- quantmine-0.2.0.dist-info/METADATA +222 -0
- quantmine-0.2.0.dist-info/RECORD +14 -0
- quantmine-0.2.0.dist-info/WHEEL +4 -0
- quantmine-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from .factor_register import factor_register
|
|
3
|
+
|
|
4
|
+
@factor_register('daily_return')
|
|
5
|
+
def daily_return(close:pd.DataFrame, tickers:list)->pd.DataFrame:
|
|
6
|
+
"""Compute daily percentage returns for a list of tickers.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
df: Price dataframe indexed by date.
|
|
10
|
+
tickers: Tickers to compute returns for.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
A dataframe of daily returns with one column per ticker.
|
|
14
|
+
|
|
15
|
+
Notes:
|
|
16
|
+
The first row for each ticker will be ``NaN`` because percentage change
|
|
17
|
+
requires a previous observation.
|
|
18
|
+
"""
|
|
19
|
+
cols = [t for t in tickers if t in close.columns]
|
|
20
|
+
daily_return_df = close[cols].pct_change()
|
|
21
|
+
return daily_return_df
|
|
22
|
+
|
|
23
|
+
@factor_register("excess_return")
|
|
24
|
+
def excess_return(daily_return: pd.DataFrame) -> pd.DataFrame:
|
|
25
|
+
"""Compute excess returns over the market proxy SPY.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
daily_return: Dataframe of daily returns that includes an ``SPY`` column.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
A dataframe of excess returns for all non-SPY columns.
|
|
32
|
+
|
|
33
|
+
Notes:
|
|
34
|
+
This function assumes ``SPY`` is the benchmark and removes it from the
|
|
35
|
+
output.
|
|
36
|
+
"""
|
|
37
|
+
market_return=daily_return['SPY']
|
|
38
|
+
excess_return=daily_return.drop(columns=['SPY']).sub(market_return, axis=0)
|
|
39
|
+
return excess_return
|
|
40
|
+
|
|
41
|
+
@factor_register('momentum')
|
|
42
|
+
def momentum(close: pd.DataFrame, tickers: list, day:int =2) -> pd.DataFrame:
|
|
43
|
+
"""Compute a simple momentum factor for each ticker.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
df: Price dataframe indexed by date.
|
|
47
|
+
tickers: Tickers to process.
|
|
48
|
+
day: Lookback window used to measure momentum.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A dataframe with columns named like ``{day}DayMomentum_TICKER``.
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
The factor is based on the relative price change between the current
|
|
55
|
+
price and the price ``day - 1`` periods ago.
|
|
56
|
+
"""
|
|
57
|
+
cols = [t for t in tickers if t in close.columns]
|
|
58
|
+
mmt = close[cols].pct_change(day - 1)
|
|
59
|
+
return mmt
|
|
60
|
+
|
|
61
|
+
# def EWMA(window_data, wk: float)->float: #做.apply()时由于切分下来的nparray会直接传入第一个参数,因此要把window_data写在最前面
|
|
62
|
+
# """Compute an exponential weighted moving average over a window.
|
|
63
|
+
|
|
64
|
+
# Args:
|
|
65
|
+
# window_data: One-dimensional numpy array passed by ``rolling.apply``.
|
|
66
|
+
# wk: Exponential decay factor.
|
|
67
|
+
|
|
68
|
+
# Returns:
|
|
69
|
+
# The weighted sum for the supplied window.
|
|
70
|
+
|
|
71
|
+
# Notes:
|
|
72
|
+
# The function is designed for use with ``Series.rolling(...).apply(...)``
|
|
73
|
+
# and expects the newest observation to contribute the most weight.
|
|
74
|
+
# """
|
|
75
|
+
# ewma=0
|
|
76
|
+
# period = len(window_data)
|
|
77
|
+
# for i in range(period):
|
|
78
|
+
# ewma += wk**(i)*window_data[period-i-1]
|
|
79
|
+
# return ewma
|
|
80
|
+
|
|
81
|
+
@factor_register('ShortTermReversal')
|
|
82
|
+
def ShortTermReversal(excess_return: pd.DataFrame,tickers: list, halflife: int, period: int)->pd.DataFrame:
|
|
83
|
+
"""Compute a short-term reversal factor from lagged excess returns.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
excess_return: Dataframe of excess returns.
|
|
87
|
+
tickers: Tickers to process.
|
|
88
|
+
halflife: Halflife used to define the exponential decay weight.
|
|
89
|
+
period: Rolling window length.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A dataframe with one short-term reversal column per ticker.
|
|
93
|
+
|
|
94
|
+
Notes:
|
|
95
|
+
The signal is negated because high recent returns are assumed to mean
|
|
96
|
+
lower near-term future returns.
|
|
97
|
+
"""
|
|
98
|
+
cols = [t for t in tickers if t in excess_return.columns]
|
|
99
|
+
data = excess_return[cols]
|
|
100
|
+
wk = 0.5 ** (1 / halflife)
|
|
101
|
+
# 固定权重FIR滤波: sum_i wk^i * x.shift(i), 每个lag对整个frame做一次shift,
|
|
102
|
+
# 等价于rolling(period).apply(EWMA)但避免了逐窗口的Python循环
|
|
103
|
+
# (O(dates*tickers*period)次Python调用 -> period次向量化操作)。
|
|
104
|
+
# NaN语义一致: 窗口内任一观测缺失则结果为NaN。
|
|
105
|
+
ewma = data * 1.0
|
|
106
|
+
for i in range(1, period):
|
|
107
|
+
ewma = ewma + (wk ** i) * data.shift(i)
|
|
108
|
+
return -ewma #反转因子信号是负的——过去收益率高,预期未来会回落
|
|
109
|
+
|
|
110
|
+
'''
|
|
111
|
+
def get_short_term_reversal_ewma(excess_return, ticker, period, halflife):
|
|
112
|
+
if ticker not in excess_return.columns:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
price = excess_return[ticker]
|
|
116
|
+
|
|
117
|
+
def ewma_func(window):
|
|
118
|
+
weights = np.array([0.5**(1/halflife) ** (len(window) - 1 - i) for i in range(len(window))])
|
|
119
|
+
weights /= weights.sum() #做权重归一化,不做归一化计算出来的EWMA值会受窗口期长短影响——窗口越长,权重之和越大,算出来的值越大,不同窗口期的值没有可比性
|
|
120
|
+
return -np.dot(weights, window)
|
|
121
|
+
|
|
122
|
+
return price.rolling(period).apply(ewma_func, raw=True)
|
|
123
|
+
'''
|
|
124
|
+
|
|
125
|
+
@factor_register('TwentyDayVolatility')
|
|
126
|
+
def TwentyDayVolatility(daily_return:pd.DataFrame, tickers:list)->pd.DataFrame: #获取20日波动率
|
|
127
|
+
"""Compute 20-day rolling volatility for each ticker.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
daily_return: Dataframe of daily returns.
|
|
131
|
+
tickers: Tickers to process.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
A dataframe of 20-day rolling standard deviations.
|
|
135
|
+
"""
|
|
136
|
+
cols = [t for t in tickers if t in daily_return.columns]
|
|
137
|
+
twenty_day_volatility = daily_return[cols].rolling(20).std()
|
|
138
|
+
return twenty_day_volatility
|
|
139
|
+
|
|
140
|
+
@factor_register('TwentyDayNegVotality')
|
|
141
|
+
def TwentyDayNegVotality(daily_return:pd.DataFrame, tickers:list)->pd.DataFrame: #获取20日负收益波动率
|
|
142
|
+
"""Compute 20-day rolling volatility using only negative returns.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
daily_return: Dataframe of daily returns.
|
|
146
|
+
tickers: Tickers to process.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
A dataframe of rolling standard deviations calculated from negative
|
|
150
|
+
returns only.
|
|
151
|
+
|
|
152
|
+
Notes:
|
|
153
|
+
Non-negative returns are converted to ``NaN`` before rolling.
|
|
154
|
+
"""
|
|
155
|
+
cols = [t for t in tickers if t in daily_return.columns]
|
|
156
|
+
neg_return = daily_return[cols].where(daily_return[cols] < 0) #满足cond的保留,不满足的变成NaN
|
|
157
|
+
return neg_return.rolling(window=20, min_periods=1).std()
|
|
158
|
+
|
|
159
|
+
@factor_register('TwentyDayAvgVol')
|
|
160
|
+
def TwentyDayAvgVol(volume:pd.DataFrame, tickers:list)->pd.DataFrame: #20日平均成交量因子
|
|
161
|
+
"""Compute 20-day average trading volume for each ticker.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
volume: Volume dataframe indexed by date.
|
|
165
|
+
tickers: Tickers to process.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
A dataframe of 20-day rolling mean volume values.
|
|
169
|
+
"""
|
|
170
|
+
cols = [t for t in tickers if t in volume.columns]
|
|
171
|
+
volume_avg = volume[cols].rolling(20).mean()
|
|
172
|
+
return volume_avg
|
|
173
|
+
|
|
174
|
+
@factor_register('VolPriceCorr')
|
|
175
|
+
def VolPriceCorr(volume:pd.DataFrame, daily_return:pd.DataFrame, tickers:list)->pd.DataFrame: #20日量价相关系数
|
|
176
|
+
"""Compute 20-day rolling correlation between returns and volume.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
volume: Volume dataframe indexed by date.
|
|
180
|
+
daily_return: Daily return dataframe indexed by date.
|
|
181
|
+
tickers: Tickers to process.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
A dataframe of rolling correlation values for each ticker.
|
|
185
|
+
|
|
186
|
+
Notes:
|
|
187
|
+
This factor measures the relationship between trading activity and
|
|
188
|
+
price movement over a 20-day window.
|
|
189
|
+
"""
|
|
190
|
+
cols = [t for t in tickers if t in daily_return.columns and t in volume.columns]
|
|
191
|
+
vol_price_corr = daily_return[cols].rolling(20).corr(volume[cols])
|
|
192
|
+
return vol_price_corr
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from . import datareader as dr
|
|
4
|
+
|
|
5
|
+
FACTOR_REGISTRY={}
|
|
6
|
+
|
|
7
|
+
def factor_register(name:str):
|
|
8
|
+
def decorate(func):
|
|
9
|
+
if name in FACTOR_REGISTRY:
|
|
10
|
+
raise ValueError(f'{name} has already sign in, can not be registered again')
|
|
11
|
+
FACTOR_REGISTRY[name] = func
|
|
12
|
+
return func
|
|
13
|
+
return decorate
|
|
14
|
+
|
|
15
|
+
def call_single_factors(func, param_pool: dict):
|
|
16
|
+
sig = inspect.signature(func)
|
|
17
|
+
kwargs = {}
|
|
18
|
+
for name, param in sig.parameters.items():
|
|
19
|
+
if name in param_pool:
|
|
20
|
+
kwargs[name] = param_pool[name]
|
|
21
|
+
elif param.default is not inspect.Parameter.empty:
|
|
22
|
+
kwargs[name] = param.default
|
|
23
|
+
else:
|
|
24
|
+
raise KeyError(f"loss value for '{name}'")
|
|
25
|
+
return func(**kwargs)
|
|
26
|
+
|
|
27
|
+
def calculate_all_factors(param_pool: dict)-> dict:
|
|
28
|
+
result ={}
|
|
29
|
+
failures = {}
|
|
30
|
+
for factor_name, func in FACTOR_REGISTRY.items():
|
|
31
|
+
try:
|
|
32
|
+
result[factor_name] = call_single_factors(func, param_pool)
|
|
33
|
+
except KeyError as e:
|
|
34
|
+
print(f'factor {factor_name} lack kwargs: {e}')
|
|
35
|
+
failures[factor_name] = str(e)
|
|
36
|
+
pending, completed = try_loop(failures, result, param_pool)
|
|
37
|
+
print(f"still failure: {pending}")
|
|
38
|
+
return pending ,completed
|
|
39
|
+
|
|
40
|
+
def try_loop(failure: dict, result: dict, param_pool:dict):
|
|
41
|
+
pending = failure.copy()
|
|
42
|
+
completed = result.copy()
|
|
43
|
+
while pending:
|
|
44
|
+
length = len(pending)
|
|
45
|
+
for factor_name in list(pending.keys()):
|
|
46
|
+
param_pool_update = {**param_pool, **completed}
|
|
47
|
+
try:
|
|
48
|
+
completed[factor_name] = call_single_factors(FACTOR_REGISTRY[factor_name], param_pool_update)
|
|
49
|
+
del pending[factor_name]
|
|
50
|
+
except KeyError:
|
|
51
|
+
continue
|
|
52
|
+
if len(pending) == length:
|
|
53
|
+
#一整轮无进展说明剩余因子的依赖永远无法满足:
|
|
54
|
+
#全部标记失败后必须退出while, 否则死循环
|
|
55
|
+
#(旧写法只标记第一个且break不出while, 下一轮None混入param_pool会引发未捕获的TypeError)
|
|
56
|
+
for factor_name in pending:
|
|
57
|
+
completed[factor_name] = None
|
|
58
|
+
print(f"factor {factor_name} failed: unresolved dependencies")
|
|
59
|
+
break
|
|
60
|
+
return pending, completed
|
|
61
|
+
|
|
62
|
+
def build_param_pool(data: dr.MarketData, tickers: list = None, **extra_param)->dict:
|
|
63
|
+
param_pool = {}
|
|
64
|
+
if tickers is None:
|
|
65
|
+
param_pool['tickers'] = data.close.columns
|
|
66
|
+
else:
|
|
67
|
+
param_pool['tickers'] = tickers
|
|
68
|
+
if data.close is not None:
|
|
69
|
+
param_pool['close'] = data.close
|
|
70
|
+
if data.volume is not None:
|
|
71
|
+
param_pool['volume'] = data.volume
|
|
72
|
+
param_pool.update(extra_param)
|
|
73
|
+
return param_pool
|