daily update
This commit is contained in:
@ -0,0 +1,404 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import vectorbtpro as vbt
|
||||
import talib
|
||||
from numba import njit
|
||||
from pathlib import Path
|
||||
import scipy
|
||||
import itertools
|
||||
|
||||
FX_MAJOR_LIST = ['EURUSD','AUDNZD','AUDUSD','AUDJPY','EURCHF','EURGBP','EURJPY','GBPCHF','GBPJPY','GBPUSD','NZDUSD','USDCAD','USDCHF','USDJPY','CADJPY','EURAUD','CHFJPY','EURCAD','AUDCAD','AUDCHF','CADCHF','EURNZD','GBPAUD','GBPCAD','GBPNZD','NZDCAD','NZDCHF','NZDJPY']
|
||||
FX_MAJOR_LIST = sorted(FX_MAJOR_LIST)
|
||||
FX_MAJOR_PATH = 'Data/FOREX/oanda/majors_{0}/{1}.csv'
|
||||
|
||||
# Major Currency Pair Loader
|
||||
def get_fx_majors(datapath=FX_MAJOR_PATH, side='bid', start=None, end=None, fillna=True):
|
||||
if side.lower() not in ['bid', 'ask']:
|
||||
raise ValueError('Side *{0}* not recognized. Must be bid or ask'.format(side))
|
||||
print('Loading FOREX {0} major pairs.'.format(side.upper()))
|
||||
if '{0}' in datapath:
|
||||
data = vbt.CSVData.fetch([datapath.format(side.lower(), i) for i in FX_MAJOR_LIST], start=start, end=end)
|
||||
else:
|
||||
data = vbt.CSVData.fetch(['{0}/majors_{1}/{2}.csv'.format(datapath, side, i) for i in FX_MAJOR_LIST], start=start, end=end)
|
||||
|
||||
return data
|
||||
|
||||
# FOREX Position Sizing with ATR
|
||||
def get_fx_position_size(data, init_cash=10_000, risk=0.01, atr=14, sl=1.5):
|
||||
atr = vbt.talib("ATR").run(
|
||||
data.high,
|
||||
data.low,
|
||||
data.close,
|
||||
timeperiod=atr,
|
||||
skipna=True
|
||||
).real.droplevel(0, axis=1)
|
||||
pip_decimal = {i:0.01 if 'JPY' in i else 0.0001 for i in data.close.columns}
|
||||
pip_value = {i:100 if 'JPY' in i else 10_000 for i in data.close.columns}
|
||||
stop_pips = np.ceil(sl*atr/pip_decimal)
|
||||
|
||||
size = ((risk*init_cash/stop_pips)*pip_value)
|
||||
|
||||
return size
|
||||
|
||||
|
||||
# NNFX Dynamic Risk Model
|
||||
@njit
|
||||
def adjust_func_nb(c, atr, sl, tp):
|
||||
position_now = c.last_position[c.col]
|
||||
|
||||
# Check if position is open and needs to be managed
|
||||
if position_now != 0:
|
||||
# Get Current SL & TP Info
|
||||
sl_info = c.last_sl_info[c.col]
|
||||
tp_info = c.last_tp_info[c.col]
|
||||
tp_info.ladder = True
|
||||
tsl_info = c.last_tsl_info[c.col]
|
||||
|
||||
last_order = c.order_records[c.order_counts[c.col] - 1, c.col]
|
||||
|
||||
if last_order.stop_type == -1:
|
||||
# STOP TYPE == -1, User Generate Order
|
||||
# Get Current ATR Value
|
||||
catr = vbt.pf_nb.select_nb(c, atr)
|
||||
|
||||
if not vbt.pf_nb.is_stop_info_active_nb(sl_info):
|
||||
sl_info.stop = catr*vbt.pf_nb.select_nb(c, sl)
|
||||
|
||||
if not vbt.pf_nb.is_stop_info_active_nb(tp_info):
|
||||
tp_info.stop = catr*vbt.pf_nb.select_nb(c, tp)
|
||||
tp_info.exit_size = round(abs(position_now) * 0.5)
|
||||
|
||||
elif last_order.stop_type == vbt.sig_enums.StopType.TP:
|
||||
# STOP TYPE == 3, last fill was a take profit
|
||||
if not vbt.pf_nb.is_stop_info_active_nb(tsl_info):
|
||||
# Set a Trailing Stop for remaining
|
||||
tsl_info.stop = sl_info.stop
|
||||
|
||||
# Deactivate Original Stop
|
||||
sl_info.stop = np.nan
|
||||
|
||||
def get_NNFX_risk(atr_, sl_, tp_):
|
||||
args = {"adjust_func_nb":adjust_func_nb,
|
||||
"adjust_args":(vbt.Rep("atr"), vbt.Rep("sl"), vbt.Rep("tp")),
|
||||
"broadcast_named_args":dict(
|
||||
atr=atr_,
|
||||
sl=sl_,
|
||||
tp=tp_
|
||||
),
|
||||
"use_stops":True}
|
||||
|
||||
return args
|
||||
|
||||
|
||||
# Whites Reality Check for a Single Strategy
|
||||
def col_p_val_WRC(col, means, n, inds):
|
||||
samples = col.values[inds].mean(axis=0)
|
||||
return (samples > means[col.name]).sum()/n
|
||||
|
||||
def get_WRC_p_val(raw_ret, allocations, n=2000):
|
||||
# Detrending & Zero Centering
|
||||
#raw_ret = np.log(data.close/data.open)
|
||||
det_ret = raw_ret - raw_ret.mean(axis=0)
|
||||
det_strat = np.sign(allocations+allocations.shift(1))*det_ret
|
||||
|
||||
# Zero Centering
|
||||
mean_strat = det_strat.mean(axis=0)
|
||||
zero_strat = det_strat - mean_strat
|
||||
|
||||
# Sampling
|
||||
inds = np.random.randint(0, raw_ret.shape[0], size=(raw_ret.shape[0], n))
|
||||
ps = zero_strat.apply(col_p_val_WRC, axis=0, args=(mean_strat, n, inds))
|
||||
|
||||
return ps
|
||||
|
||||
# Monte Carlo Permutation Method (MCP) for Inference Testing
|
||||
def col_p_val_MCP(col, det_ret, means, inds, n):
|
||||
samples = det_ret[col.name[-1]].values[inds]
|
||||
#print(col.values[:, np.newaxis].shape)
|
||||
samples = np.nanmean(samples*col.values[:, np.newaxis], axis=0)
|
||||
return (samples > means[col.name]).sum()/n
|
||||
|
||||
def get_MCP_p_val(raw_ret, allocations, n=2000):
|
||||
# Detrending
|
||||
det_ret = raw_ret - raw_ret.mean(axis=0)
|
||||
allocations = np.sign(allocations + allocations.shift(1))
|
||||
det_strat = allocations*det_ret
|
||||
|
||||
# Zero Centering
|
||||
mean_strat = det_strat.mean(axis=0)
|
||||
|
||||
# Sampling
|
||||
inds = np.tile(np.arange(0, raw_ret.shape[0])[:, np.newaxis], (1, 2000))
|
||||
inds = np.take_along_axis(inds, np.random.randn(*inds.shape).argsort(axis=0), axis=0)
|
||||
ps = allocations.apply(col_p_val_MCP, axis=0, args=(det_ret, mean_strat, inds, n))
|
||||
|
||||
return ps
|
||||
|
||||
def _nonull_df_dict(df, times=True):
|
||||
if times:
|
||||
d = {i:df[i].dropna().to_numpy(dtype=int) for i in df.columns}
|
||||
else:
|
||||
d = {i:df[i].dropna().to_numpy() for i in df.columns}
|
||||
return d
|
||||
|
||||
def col_p_val_MCPH(col, det_ret, means, times, signals, n):
|
||||
# Get Column Specific Holding Times & Signals
|
||||
_times = times[col.name]
|
||||
_signals = signals[col.name]
|
||||
|
||||
# Create Time/Signal Perumutation Arrays
|
||||
index_arr = np.tile(np.arange(0, len(_times))[:, np.newaxis], (1, n))
|
||||
sorter = np.random.randn(*index_arr.shape)
|
||||
index_arr = np.take_along_axis(index_arr, sorter.argsort(axis=0), axis=0)
|
||||
|
||||
# Create Sampling Array
|
||||
_times_perm = _times[index_arr]
|
||||
_signals_perm = _signals[index_arr]
|
||||
_times_flat = _times_perm.flatten('F')
|
||||
_signals_flat = _signals_perm.flatten('F')
|
||||
samples = np.repeat(_signals_flat, _times_flat).reshape((len(col), n), order='F')
|
||||
samples = np.multiply(det_ret[col.name[-1]].values[np.tile(np.arange(0, col.shape[0])[:, np.newaxis], (1, n))], samples)
|
||||
|
||||
samples = np.nanmean(samples, axis=0)
|
||||
|
||||
return (samples > means[col.name]).sum()/n
|
||||
|
||||
def get_MCPH_p_val(raw_ret, allocations, n=2000):
|
||||
# Detrending
|
||||
#raw_ret = np.log(data.close/data.open)
|
||||
det_ret = raw_ret - raw_ret.mean(axis=0)
|
||||
allocations = np.sign(allocations + allocations.shift(1)).fillna(0)
|
||||
det_strat = allocations*det_ret
|
||||
|
||||
# Zero Centering
|
||||
mean_strat = det_strat.mean(axis=0)
|
||||
|
||||
# Strategy Allocation Holding Distribution and Corresponding Signals
|
||||
changes = (allocations == allocations.shift(1))
|
||||
times = changes.cumsum()-changes.cumsum().where(~changes).ffill().fillna(0).astype(int) + 1
|
||||
times = times[times - times.shift(-1, fill_value=1) >= 0]
|
||||
signals = allocations[~times.isnull()]
|
||||
|
||||
# Get Dictionary of Times/Signals
|
||||
times = _nonull_df_dict(times)
|
||||
signals = _nonull_df_dict(signals, times=False)
|
||||
|
||||
# Sampling
|
||||
ps = allocations.apply(col_p_val_MCPH, axis=0, args=(det_ret, mean_strat, times, signals, n))
|
||||
|
||||
return ps
|
||||
|
||||
# Adjusted Returns: Adjusts closes of time series to reflect trade exit prices. Used as input to WRC and MCP statistical tests
|
||||
def get_adjusted_returns(data, pf):
|
||||
# Trade Records
|
||||
records = pf.trades.records_readable[['Column', 'Exit Index', 'Avg Exit Price']]
|
||||
records.Column=records['Column'].apply(lambda x: x[-1])
|
||||
|
||||
close_adj = data.get('Close')
|
||||
for row, value in records.iterrows():
|
||||
close_adj[value['Column']][value['Exit Index']] = value['Avg Exit Price']
|
||||
|
||||
return np.log(close_adj/data.open)
|
||||
|
||||
# Optimized Split
|
||||
def get_optimized_split(tf, frac, n):
|
||||
# Parameter Estimation
|
||||
d = tf/(frac + n*(1 - frac))
|
||||
di = frac*d
|
||||
do = (1-frac)*d
|
||||
|
||||
# Mixed Integer, Linear Optimization
|
||||
c = [-(1/frac - 1), 1]
|
||||
Aeq = [[1, n]]
|
||||
Aub = [[-1, 1],
|
||||
[(1/frac - 1), -1]]
|
||||
beq = [tf]
|
||||
bub = [0, 0]
|
||||
x0_bounds = (di*0.5, di*1.5)
|
||||
x1_bounds = (do*0.5, do*1.5)
|
||||
res = scipy.optimize.linprog(
|
||||
c, A_eq=Aeq, b_eq=beq, A_ub=Aub, b_ub=bub, bounds=(x0_bounds, x1_bounds),
|
||||
integrality=[1, 1],
|
||||
method='highs',
|
||||
options={"disp": True})
|
||||
|
||||
# Solutions
|
||||
di, do = res.x
|
||||
|
||||
# Actual Fraction
|
||||
frac_a = di/(do+di)
|
||||
|
||||
return int(di), int(do), frac_a
|
||||
|
||||
def wfo_split_func(splits, bounds, index, length_IS=20, length_OOS=30):
|
||||
if len(splits) == 0:
|
||||
new_split = (slice(0, length_IS), slice(length_IS, length_OOS+length_IS))
|
||||
else:
|
||||
# Previous split, second set, right bound
|
||||
prev_end = bounds[-1][1][1]
|
||||
|
||||
# Split Calculation
|
||||
new_split = (
|
||||
slice(prev_end-length_IS, prev_end),
|
||||
slice(prev_end, prev_end + length_OOS)
|
||||
)
|
||||
if new_split[-1].stop > len(index):
|
||||
return None
|
||||
return new_split
|
||||
|
||||
def get_wfo_splitter(index, fraction, n):
|
||||
# Generates a splitter based on train/(train+test) fraction and number of folds
|
||||
d_IS, d_OOS, frac = get_optimized_split(len(index), fraction, n)
|
||||
|
||||
# Generate the Splitter
|
||||
splitter = vbt.Splitter.from_split_func(
|
||||
index,
|
||||
wfo_split_func,
|
||||
split_args=(
|
||||
vbt.Rep("splits"),
|
||||
vbt.Rep("bounds"),
|
||||
vbt.Rep("index"),
|
||||
),
|
||||
split_kwargs={
|
||||
'length_IS':d_IS,
|
||||
'length_OOS':d_OOS
|
||||
},
|
||||
set_labels=["IS", "OOS"]
|
||||
)
|
||||
|
||||
return splitter
|
||||
|
||||
# WFO Fold Analysis Splitters
|
||||
def get_wfo_splitters(index, fractions, folds):
|
||||
# Create Combinations of Folds/Fractions
|
||||
combinations = itertools.product(fractions, folds)
|
||||
|
||||
# Generate Splitters
|
||||
splitters = {}
|
||||
splitter_ranges = {}
|
||||
for comb in combinations:
|
||||
splitter = get_wfo_splitter(index, comb[0], comb[1])
|
||||
splitters.update({comb:splitter})
|
||||
splitter_ranges.update({comb:[d_IS, d_OOS, frac]})
|
||||
|
||||
return splitters, splitter_ranges
|
||||
|
||||
# NNFX WFO Trainin Performance Function
|
||||
@vbt.parameterized(merg_func='concat')
|
||||
def strat_perf(data, ind, atr, pos_size, long_signal='long', short_signal='short', metric='sharpe_ratio'):
|
||||
# Simulation
|
||||
pf = vbt.Portfolio.from_signals(
|
||||
data,
|
||||
entries=getattr(ind, long_signal),
|
||||
short_entries=getattr(ind, short_signal),
|
||||
**get_NNFX_risk(atr, 1.5, 1.0),
|
||||
size=pos_size,
|
||||
size_type='amount',
|
||||
init_cash=10_000,
|
||||
delta_format='absolute',
|
||||
price='nextopen',
|
||||
stop_entry_price='fillprice',
|
||||
leverage=np.inf,
|
||||
#fixed_fees=pos_size*data.get('Spread')
|
||||
)
|
||||
result = getattr(pf, metric)
|
||||
return result
|
||||
|
||||
# Walk Forward Optimization Portfolio Simulation
|
||||
def walk_forward_optimization(data, ind, pos_size, atr, splitter, metric='total_return', long_signal='long', short_signal='short', group=True):
|
||||
|
||||
# Calculate Performance on Training Sets
|
||||
train_perf = splitter.apply(
|
||||
strat_perf,
|
||||
vbt.Takeable(data),
|
||||
vbt.Takeable(ind),
|
||||
vbt.Takeable(atr),
|
||||
vbt.Takeable(pos_size),
|
||||
metric=metric,
|
||||
long_signal=long_signal,
|
||||
short_signal=short_signal,
|
||||
_execute_kwargs=dict(
|
||||
show_progress=False,
|
||||
#clear_cache=50,
|
||||
#collect_garbage=50
|
||||
),
|
||||
merge_func='row_stack',
|
||||
set_='IS',
|
||||
execute_kwargs=dict(show_progress=True),
|
||||
jitted=True
|
||||
)
|
||||
|
||||
# Get the Best Parameters
|
||||
exclusions = [i for i in range(len(train_perf.index.names)) if train_perf.index.names[i] not in getattr(ind, long_signal).columns.names]
|
||||
group = train_perf.groupby(['split','symbol'])
|
||||
best = group.idxmax()
|
||||
best[:] = [tuple([i[j] for j in range(len(i)) if j not in exclusions]) for i in best]
|
||||
best = best.droplevel('symbol')
|
||||
|
||||
# Generate the OOS Signals
|
||||
opt_long = []
|
||||
opt_short = []
|
||||
for i in best.index.get_level_values('split').unique():
|
||||
_opt_long = splitter['OOS'].take(getattr(ind, long_signal))[i][best[i]]
|
||||
_opt_short = splitter['OOS'].take(getattr(ind, short_signal))[i][best[i]]
|
||||
|
||||
remove_cols = [i for i in _opt_long.columns.names if i != 'symbol']
|
||||
_opt_long = _opt_long.droplevel(remove_cols, axis=1)
|
||||
_opt_short = _opt_short.droplevel(remove_cols, axis=1)
|
||||
|
||||
opt_long.append(_opt_long)
|
||||
opt_short.append(_opt_short)
|
||||
opt_long = pd.concat(opt_long)
|
||||
opt_short = pd.concat(opt_short)
|
||||
|
||||
# Run the WFO Portfolio
|
||||
group_by = len(opt_long.columns)*[0] if group else None
|
||||
pf = vbt.Portfolio.from_signals(
|
||||
data,
|
||||
entries=opt_long,
|
||||
short_entries=opt_short,
|
||||
**get_NNFX_risk(atr, 1.5, 1.0),
|
||||
size=pos_size,
|
||||
size_type='amount',
|
||||
init_cash=10_000,
|
||||
delta_format='absolute',
|
||||
price='nextopen',
|
||||
stop_entry_price='fillprice',
|
||||
leverage=np.inf,
|
||||
#fixed_fees=pos_size*data.get('Spread'),
|
||||
group_by=group_by
|
||||
)
|
||||
|
||||
return pf
|
||||
|
||||
# WFO Fold Analysis
|
||||
def wfo_fold_analysis(data, ind, pos_size, atr, splitters, metric='total_return', long_signal='long', short_signal='short'):
|
||||
# Create the Results Matrix
|
||||
keys = splitters.keys()
|
||||
fractions = list(set([i[0] for i in keys]))
|
||||
folds = list(set([i[1] for i in keys]))
|
||||
FF, NN = np.meshgrid(fractions, folds)
|
||||
RR = np.zeros_like(FF)
|
||||
|
||||
# Perform the Analysis
|
||||
for key, splitter in splitters.items():
|
||||
# Get the Key Indices
|
||||
idx = np.where((key[0] == FF) & (key[1] == NN))
|
||||
|
||||
# WFO using Splitter
|
||||
print('Performing Walk Forward for train fraction {0} and N = {1}'.format(key[0], key[1]))
|
||||
wfo = walk_forward_optimization(data, ind, pos_size, atr, splitter, metric=metric, long_signal=long_signal, short_signal=short_signal)
|
||||
|
||||
# Correlation
|
||||
rolling_returns = pd.DataFrame(wfo.cumulative_returns)
|
||||
rolling_returns = rolling_returns[rolling_returns != 1.0].dropna()
|
||||
rolling_returns['idx'] = np.arange(0, len(rolling_returns), 1)
|
||||
rolling_returns
|
||||
corr_matrix = rolling_returns.corr()
|
||||
R_sq = corr_matrix.iloc[0, 1]**2
|
||||
|
||||
# Update the Results
|
||||
print(idx[0][0], idx[1][0], R_sq)
|
||||
RR[idx] = R_sq
|
||||
|
||||
return FF, NN, RR
|
||||
Reference in New Issue
Block a user