fa-quant 1.2.1__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.
- fa_quant-1.2.1.dist-info/METADATA +236 -0
- fa_quant-1.2.1.dist-info/RECORD +106 -0
- fa_quant-1.2.1.dist-info/WHEEL +4 -0
- fa_quant-1.2.1.dist-info/entry_points.txt +2 -0
- fa_quant-1.2.1.dist-info/licenses/LICENSE +21 -0
- financial_agent/__init__.py +1 -0
- financial_agent/agents/__init__.py +0 -0
- financial_agent/agents/claude/__init__.py +0 -0
- financial_agent/agents/gpt/__init__.py +0 -0
- financial_agent/agents/grok/__init__.py +0 -0
- financial_agent/dl/__init__.py +0 -0
- financial_agent/dl/models/__init__.py +0 -0
- financial_agent/dl/models/baseline_model.py +23 -0
- financial_agent/dl/preprocessing/__init__.py +0 -0
- financial_agent/dl/preprocessing/data_windowing.py +135 -0
- financial_agent/harness/__init__.py +73 -0
- financial_agent/harness/__main__.py +430 -0
- financial_agent/harness/_system/factor_scoring.py +175 -0
- financial_agent/harness/_system/settlement_ledger.py +1222 -0
- financial_agent/harness/audit.py +393 -0
- financial_agent/harness/equity_policy.toml +179 -0
- financial_agent/harness/fingerprint.py +55 -0
- financial_agent/harness/gates.py +522 -0
- financial_agent/harness/handlers.py +77 -0
- financial_agent/harness/helpers.py +47 -0
- financial_agent/harness/kernels.py +696 -0
- financial_agent/harness/ledger.py +325 -0
- financial_agent/harness/lifecycle.py +127 -0
- financial_agent/harness/manifest.py +113 -0
- financial_agent/harness/market_calendar.py +50 -0
- financial_agent/harness/mcp.py +505 -0
- financial_agent/harness/policy.py +248 -0
- financial_agent/harness/runner.py +275 -0
- financial_agent/harness/schema.py +451 -0
- financial_agent/harness/scoring.py +363 -0
- financial_agent/harness/skills.py +458 -0
- financial_agent/quant/__init__.py +1 -0
- financial_agent/quant/__main__.py +506 -0
- financial_agent/quant/backtest/__init__.py +16 -0
- financial_agent/quant/backtest/config.py +74 -0
- financial_agent/quant/backtest/costs.py +62 -0
- financial_agent/quant/backtest/engine.py +248 -0
- financial_agent/quant/backtest/metrics.py +102 -0
- financial_agent/quant/backtest/result.py +51 -0
- financial_agent/quant/data/__init__.py +21 -0
- financial_agent/quant/data/bars.py +103 -0
- financial_agent/quant/data/cache.py +168 -0
- financial_agent/quant/data/providers.py +138 -0
- financial_agent/quant/data/quality.py +104 -0
- financial_agent/quant/data/synthetic.py +67 -0
- financial_agent/quant/execution/__init__.py +24 -0
- financial_agent/quant/execution/alpaca.py +293 -0
- financial_agent/quant/execution/broker.py +98 -0
- financial_agent/quant/execution/ibkr.py +634 -0
- financial_agent/quant/execution/journal.py +139 -0
- financial_agent/quant/execution/paper.py +254 -0
- financial_agent/quant/execution/reconcile.py +35 -0
- financial_agent/quant/mcp.py +210 -0
- financial_agent/quant/portfolio.py +56 -0
- financial_agent/quant/quant_policy.toml +40 -0
- financial_agent/quant/risk/__init__.py +21 -0
- financial_agent/quant/risk/killswitch.py +61 -0
- financial_agent/quant/risk/limits.py +109 -0
- financial_agent/quant/risk/orders.py +91 -0
- financial_agent/quant/risk/pretrade.py +157 -0
- financial_agent/quant/risk/state.py +86 -0
- financial_agent/quant/runner/__init__.py +20 -0
- financial_agent/quant/runner/config.py +254 -0
- financial_agent/quant/runner/promotion.py +277 -0
- financial_agent/quant/runner/report.py +231 -0
- financial_agent/quant/runner/session.py +705 -0
- financial_agent/quant/strategies/__init__.py +19 -0
- financial_agent/quant/strategies/base.py +44 -0
- financial_agent/quant/strategies/benchmarks.py +54 -0
- financial_agent/quant/strategies/momentum.py +80 -0
- financial_agent/quant/strategies/registry.py +65 -0
- financial_agent/quant/strategies/research.py +226 -0
- financial_agent/quant/strategies/trend.py +91 -0
- financial_agent/quant/validation/__init__.py +41 -0
- financial_agent/quant/validation/bootstrap.py +39 -0
- financial_agent/quant/validation/certificate.py +189 -0
- financial_agent/quant/validation/gate.py +304 -0
- financial_agent/quant/validation/overfitting.py +148 -0
- financial_agent/quant/validation/stress.py +33 -0
- financial_agent/quant/validation/walkforward.py +157 -0
- financial_agent/rl/__init__.py +13 -0
- financial_agent/rl/agent.py +112 -0
- financial_agent/rl/environment.py +177 -0
- financial_agent/rl/trainer.py +142 -0
- financial_agent/trading/__init__.py +0 -0
- financial_agent/trading/autotrain/__init__.py +0 -0
- financial_agent/trading/autotrain/training.py +92 -0
- financial_agent/trading/backtest.py +118 -0
- financial_agent/trading/config/__init__.py +0 -0
- financial_agent/trading/config/config.py +1587 -0
- financial_agent/trading/marketdata/__init__.py +0 -0
- financial_agent/trading/marketdata/yahoodownloader.py +99 -0
- financial_agent/trading/model/__init__.py +0 -0
- financial_agent/trading/model/models.py +466 -0
- financial_agent/trading/preprocessing/__init__.py +0 -0
- financial_agent/trading/preprocessing/data.py +32 -0
- financial_agent/trading/preprocessing/preprocessors.py +162 -0
- financial_agent/trading/utils/__init__.py +0 -0
- financial_agent/trading/utils/gpu_config.py +213 -0
- financial_agent/trading/utils/main.py +52 -0
- financial_agent/trading/utils/setup_gpu_for_notebooks.py +130 -0
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Contains methods and classes to collect data from
|
|
2
|
+
Yahoo Finance API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import yfinance as yf
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class YahooDownloader:
|
|
10
|
+
"""Provides methods for retrieving daily stock data from
|
|
11
|
+
Yahoo Finance API
|
|
12
|
+
|
|
13
|
+
Attributes
|
|
14
|
+
----------
|
|
15
|
+
start_date : str
|
|
16
|
+
start date of the data (modified from config.py)
|
|
17
|
+
end_date : str
|
|
18
|
+
end date of the data (modified from config.py)
|
|
19
|
+
ticker_list : list
|
|
20
|
+
a list of stock tickers (modified from config.py)
|
|
21
|
+
|
|
22
|
+
Methods
|
|
23
|
+
-------
|
|
24
|
+
fetch_data()
|
|
25
|
+
Fetches data from yahoo API
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, start_date: str, end_date: str, ticker_list: list):
|
|
30
|
+
|
|
31
|
+
self.start_date = start_date
|
|
32
|
+
self.end_date = end_date
|
|
33
|
+
self.ticker_list = ticker_list
|
|
34
|
+
|
|
35
|
+
def fetch_data(self) -> pd.DataFrame:
|
|
36
|
+
"""Fetches data from Yahoo API
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
`pd.DataFrame`
|
|
43
|
+
7 columns: A date, open, high, low, close, volume and tick symbol
|
|
44
|
+
for the specified stock ticker
|
|
45
|
+
"""
|
|
46
|
+
# Download and save the data in a pandas DataFrame:
|
|
47
|
+
data_df = pd.DataFrame()
|
|
48
|
+
for tic in self.ticker_list:
|
|
49
|
+
temp_df = yf.download(tic, start=self.start_date, end=self.end_date, auto_adjust=False, multi_level_index=False)
|
|
50
|
+
temp_df["tic"] = tic
|
|
51
|
+
data_df = pd.concat([data_df, temp_df])
|
|
52
|
+
|
|
53
|
+
# reset the index, we want to use numbers as index instead of dates
|
|
54
|
+
data_df = data_df.reset_index()
|
|
55
|
+
try:
|
|
56
|
+
# convert the column names to standardized names
|
|
57
|
+
|
|
58
|
+
mapping = {
|
|
59
|
+
"Date": "date",
|
|
60
|
+
"Adj Close": "adjcp",
|
|
61
|
+
"Close": "close",
|
|
62
|
+
"High": "high",
|
|
63
|
+
"Low": "low",
|
|
64
|
+
"Open": "open",
|
|
65
|
+
"Volume": "volume",
|
|
66
|
+
"tic": "tic"
|
|
67
|
+
}
|
|
68
|
+
data_df = data_df.rename(columns=mapping)
|
|
69
|
+
|
|
70
|
+
# use adjusted close price instead of close price
|
|
71
|
+
data_df["close"] = data_df["adjcp"]
|
|
72
|
+
# drop the adjusted close price column
|
|
73
|
+
data_df = data_df.drop(['adjcp'], axis=1)
|
|
74
|
+
except NotImplementedError:
|
|
75
|
+
print("the features are not supported currently")
|
|
76
|
+
# create day of the week column (monday = 0)
|
|
77
|
+
data_df["day"] = data_df["date"].dt.dayofweek
|
|
78
|
+
# convert date to standard string format, easy to filter
|
|
79
|
+
data_df["date"] = data_df.date.apply(lambda x: x.strftime("%Y-%m-%d"))
|
|
80
|
+
# drop missing data
|
|
81
|
+
data_df = data_df.dropna()
|
|
82
|
+
data_df = data_df.reset_index(drop=True)
|
|
83
|
+
print("Shape of DataFrame: ", data_df.shape)
|
|
84
|
+
# print("Display DataFrame: ", data_df.head())
|
|
85
|
+
|
|
86
|
+
data_df = data_df.sort_values(by=['date','tic']).reset_index(drop=True)
|
|
87
|
+
|
|
88
|
+
return data_df
|
|
89
|
+
|
|
90
|
+
def select_equal_rows_stock(self, df):
|
|
91
|
+
df_check = df.tic.value_counts()
|
|
92
|
+
df_check = pd.DataFrame(df_check).reset_index()
|
|
93
|
+
df_check.columns = ["tic", "counts"]
|
|
94
|
+
mean_df = df_check.counts.mean()
|
|
95
|
+
equal_list = list(df.tic.value_counts() >= mean_df)
|
|
96
|
+
names = df.tic.value_counts().index
|
|
97
|
+
select_stocks_list = list(names[equal_list])
|
|
98
|
+
df = df[df.tic.isin(select_stocks_list)]
|
|
99
|
+
return df
|
|
File without changes
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# common library
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import time
|
|
5
|
+
import gymnasium as gym
|
|
6
|
+
|
|
7
|
+
# RL models from stable-baselines
|
|
8
|
+
# from stable_baselines import SAC
|
|
9
|
+
# from stable_baselines import TD3
|
|
10
|
+
|
|
11
|
+
from stable_baselines3.ppo import MlpPolicy
|
|
12
|
+
from stable_baselines3.common.vec_env import DummyVecEnv
|
|
13
|
+
|
|
14
|
+
from stable_baselines3 import DDPG
|
|
15
|
+
from stable_baselines3.common.noise import (
|
|
16
|
+
NormalActionNoise,
|
|
17
|
+
OrnsteinUhlenbeckActionNoise,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ..config import config
|
|
21
|
+
from ..preprocessing.data import data_split
|
|
22
|
+
from ..env.env_stocktrading import StockTradingEnv
|
|
23
|
+
|
|
24
|
+
from stable_baselines3 import A2C
|
|
25
|
+
from stable_baselines3 import PPO
|
|
26
|
+
from stable_baselines3 import TD3
|
|
27
|
+
from stable_baselines3.td3.policies import MlpPolicy
|
|
28
|
+
from stable_baselines3.common.noise import (
|
|
29
|
+
NormalActionNoise,
|
|
30
|
+
OrnsteinUhlenbeckActionNoise,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from stable_baselines3 import SAC
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
MODELS = {"a2c": A2C, "ddpg": DDPG, "td3": TD3, "sac": SAC, "ppo": PPO}
|
|
37
|
+
|
|
38
|
+
MODEL_KWARGS = {x: config.__dict__[f"{x.upper()}_PARAMS"] for x in MODELS.keys()}
|
|
39
|
+
|
|
40
|
+
NOISE = {
|
|
41
|
+
"normal": NormalActionNoise,
|
|
42
|
+
"ornstein_uhlenbeck": OrnsteinUhlenbeckActionNoise,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DRLAgent:
|
|
47
|
+
"""Provides implementations for DRL algorithms
|
|
48
|
+
|
|
49
|
+
Attributes
|
|
50
|
+
----------
|
|
51
|
+
env: gym environment class
|
|
52
|
+
user-defined class
|
|
53
|
+
|
|
54
|
+
Methods
|
|
55
|
+
-------
|
|
56
|
+
train_PPO()
|
|
57
|
+
the implementation for PPO algorithm
|
|
58
|
+
train_A2C()
|
|
59
|
+
the implementation for A2C algorithm
|
|
60
|
+
train_DDPG()
|
|
61
|
+
the implementation for DDPG algorithm
|
|
62
|
+
train_TD3()
|
|
63
|
+
the implementation for TD3 algorithm
|
|
64
|
+
train_SAC()
|
|
65
|
+
the implementation for SAC algorithm
|
|
66
|
+
DRL_prediction()
|
|
67
|
+
make a prediction in a test dataset and get results
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def DRL_prediction(model, environment):
|
|
72
|
+
test_env, test_obs = environment.get_sb_env()
|
|
73
|
+
"""make a prediction"""
|
|
74
|
+
account_memory = []
|
|
75
|
+
actions_memory = []
|
|
76
|
+
test_obs = test_env.reset()
|
|
77
|
+
for i in range(len(environment.df.index.unique())):
|
|
78
|
+
action, _states = model.predict(test_obs)
|
|
79
|
+
#account_memory = test_env.env_method(method_name="save_asset_memory")
|
|
80
|
+
#actions_memory = test_env.env_method(method_name="save_action_memory")
|
|
81
|
+
test_obs, rewards, dones, info = test_env.step(action)
|
|
82
|
+
if i == (len(environment.df.index.unique()) - 2):
|
|
83
|
+
account_memory = test_env.env_method(method_name="save_asset_memory")
|
|
84
|
+
actions_memory = test_env.env_method(method_name="save_action_memory")
|
|
85
|
+
if dones[0]:
|
|
86
|
+
print("hit end!")
|
|
87
|
+
break
|
|
88
|
+
return account_memory[0], actions_memory[0]
|
|
89
|
+
|
|
90
|
+
def __init__(self, env):
|
|
91
|
+
self.env = env
|
|
92
|
+
|
|
93
|
+
def get_model(
|
|
94
|
+
self,
|
|
95
|
+
model_name,
|
|
96
|
+
policy="MlpPolicy",
|
|
97
|
+
policy_kwargs=None,
|
|
98
|
+
model_kwargs=None,
|
|
99
|
+
verbose=1,
|
|
100
|
+
):
|
|
101
|
+
if model_name not in MODELS:
|
|
102
|
+
raise NotImplementedError("NotImplementedError")
|
|
103
|
+
|
|
104
|
+
if model_kwargs is None:
|
|
105
|
+
model_kwargs = MODEL_KWARGS[model_name]
|
|
106
|
+
|
|
107
|
+
if "action_noise" in model_kwargs:
|
|
108
|
+
n_actions = self.env.action_space.shape[-1]
|
|
109
|
+
model_kwargs["action_noise"] = NOISE[model_kwargs["action_noise"]](
|
|
110
|
+
mean=np.zeros(n_actions), sigma=0.1 * np.ones(n_actions)
|
|
111
|
+
)
|
|
112
|
+
print(model_kwargs)
|
|
113
|
+
model = MODELS[model_name](
|
|
114
|
+
policy=policy,
|
|
115
|
+
env=self.env,
|
|
116
|
+
tensorboard_log=f"{config.TENSORBOARD_LOG_DIR}/{model_name}",
|
|
117
|
+
verbose=verbose,
|
|
118
|
+
policy_kwargs=policy_kwargs,
|
|
119
|
+
**model_kwargs,
|
|
120
|
+
)
|
|
121
|
+
return model
|
|
122
|
+
|
|
123
|
+
def train_model(self, model, tb_log_name, total_timesteps=5000):
|
|
124
|
+
model = model.learn(total_timesteps=total_timesteps, tb_log_name=tb_log_name)
|
|
125
|
+
return model
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class DRLEnsembleAgent:
|
|
129
|
+
@staticmethod
|
|
130
|
+
def get_model(model_name,
|
|
131
|
+
env,
|
|
132
|
+
policy="MlpPolicy",
|
|
133
|
+
policy_kwargs=None,
|
|
134
|
+
model_kwargs=None,
|
|
135
|
+
verbose=1):
|
|
136
|
+
|
|
137
|
+
if model_name not in MODELS:
|
|
138
|
+
raise NotImplementedError("NotImplementedError")
|
|
139
|
+
|
|
140
|
+
if model_kwargs is None:
|
|
141
|
+
temp_model_kwargs = MODEL_KWARGS[model_name]
|
|
142
|
+
else:
|
|
143
|
+
temp_model_kwargs = model_kwargs.copy()
|
|
144
|
+
|
|
145
|
+
if "action_noise" in temp_model_kwargs:
|
|
146
|
+
n_actions = env.action_space.shape[-1]
|
|
147
|
+
temp_model_kwargs["action_noise"] = NOISE[temp_model_kwargs["action_noise"]](
|
|
148
|
+
mean=np.zeros(n_actions), sigma=0.1 * np.ones(n_actions)
|
|
149
|
+
)
|
|
150
|
+
print(temp_model_kwargs)
|
|
151
|
+
model = MODELS[model_name](
|
|
152
|
+
policy=policy,
|
|
153
|
+
env=env,
|
|
154
|
+
tensorboard_log=f"{config.TENSORBOARD_LOG_DIR}/{model_name}",
|
|
155
|
+
verbose=verbose,
|
|
156
|
+
policy_kwargs=policy_kwargs,
|
|
157
|
+
**temp_model_kwargs,
|
|
158
|
+
)
|
|
159
|
+
return model
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def train_model(model, model_name, tb_log_name, iter_num, total_timesteps=5000):
|
|
163
|
+
model = model.learn(total_timesteps=total_timesteps, tb_log_name=tb_log_name)
|
|
164
|
+
model.save(f"{config.TRAINED_MODEL_DIR}/{model_name.upper()}_{total_timesteps//1000}k_{iter_num}")
|
|
165
|
+
return model
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def get_validation_sharpe(iteration,model_name):
|
|
169
|
+
###Calculate Sharpe ratio based on validation results###
|
|
170
|
+
df_total_value = pd.read_csv('results/account_value_validation_{}_{}.csv'.format(model_name,iteration))
|
|
171
|
+
sharpe = (4 ** 0.5) * df_total_value['daily_return'].mean() / \
|
|
172
|
+
df_total_value['daily_return'].std()
|
|
173
|
+
return sharpe
|
|
174
|
+
|
|
175
|
+
def __init__(self,df,
|
|
176
|
+
train_period,val_test_period,
|
|
177
|
+
rebalance_window, validation_window,
|
|
178
|
+
stock_dim,
|
|
179
|
+
hmax,
|
|
180
|
+
initial_amount,
|
|
181
|
+
buy_cost_pct,
|
|
182
|
+
sell_cost_pct,
|
|
183
|
+
reward_scaling,
|
|
184
|
+
state_space,
|
|
185
|
+
action_space,
|
|
186
|
+
tech_indicator_list,
|
|
187
|
+
print_verbosity):
|
|
188
|
+
|
|
189
|
+
self.df=df
|
|
190
|
+
self.train_period = train_period
|
|
191
|
+
self.val_test_period = val_test_period
|
|
192
|
+
|
|
193
|
+
self.unique_trade_date = df[(df.date > val_test_period[0])&(df.date <= val_test_period[1])].date.unique()
|
|
194
|
+
self.rebalance_window = rebalance_window
|
|
195
|
+
self.validation_window = validation_window
|
|
196
|
+
|
|
197
|
+
self.stock_dim = stock_dim
|
|
198
|
+
self.hmax = hmax
|
|
199
|
+
self.initial_amount = initial_amount
|
|
200
|
+
self.buy_cost_pct = buy_cost_pct
|
|
201
|
+
self.sell_cost_pct = sell_cost_pct
|
|
202
|
+
self.reward_scaling = reward_scaling
|
|
203
|
+
self.state_space = state_space
|
|
204
|
+
self.action_space = action_space
|
|
205
|
+
self.tech_indicator_list = tech_indicator_list
|
|
206
|
+
self.print_verbosity = print_verbosity
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def DRL_validation(self, model, test_data, test_env, test_obs):
|
|
210
|
+
###validation process###
|
|
211
|
+
for i in range(len(test_data.index.unique())):
|
|
212
|
+
action, _states = model.predict(test_obs)
|
|
213
|
+
test_obs, rewards, dones, info = test_env.step(action)
|
|
214
|
+
|
|
215
|
+
def DRL_prediction(self,model,name,last_state,iter_num,turbulence_threshold,initial):
|
|
216
|
+
### make a prediction based on trained model###
|
|
217
|
+
|
|
218
|
+
## trading env
|
|
219
|
+
trade_data = data_split(self.df, start=self.unique_trade_date[iter_num - self.rebalance_window], end=self.unique_trade_date[iter_num])
|
|
220
|
+
trade_env = DummyVecEnv([lambda: StockTradingEnv(trade_data,
|
|
221
|
+
self.stock_dim,
|
|
222
|
+
self.hmax,
|
|
223
|
+
self.initial_amount,
|
|
224
|
+
self.buy_cost_pct,
|
|
225
|
+
self.sell_cost_pct,
|
|
226
|
+
self.reward_scaling,
|
|
227
|
+
self.state_space,
|
|
228
|
+
self.action_space,
|
|
229
|
+
self.tech_indicator_list,
|
|
230
|
+
turbulence_threshold=turbulence_threshold,
|
|
231
|
+
initial=initial,
|
|
232
|
+
previous_state=last_state,
|
|
233
|
+
model_name=name,
|
|
234
|
+
mode='trade',
|
|
235
|
+
iteration=iter_num,
|
|
236
|
+
print_verbosity=self.print_verbosity)])
|
|
237
|
+
|
|
238
|
+
trade_obs = trade_env.reset()
|
|
239
|
+
|
|
240
|
+
for i in range(len(trade_data.index.unique())):
|
|
241
|
+
action, _states = model.predict(trade_obs)
|
|
242
|
+
trade_obs, rewards, dones, info = trade_env.step(action)
|
|
243
|
+
if i == (len(trade_data.index.unique()) - 2):
|
|
244
|
+
# print(env_test.render())
|
|
245
|
+
last_state = trade_env.render()
|
|
246
|
+
|
|
247
|
+
df_last_state = pd.DataFrame({'last_state': last_state})
|
|
248
|
+
df_last_state.to_csv('results/last_state_{}_{}.csv'.format(name, i), index=False)
|
|
249
|
+
return last_state
|
|
250
|
+
|
|
251
|
+
def run_ensemble_strategy(self,A2C_model_kwargs,PPO_model_kwargs,DDPG_model_kwargs,timesteps_dict):
|
|
252
|
+
"""Ensemble Strategy that combines PPO, A2C and DDPG"""
|
|
253
|
+
print("============Start Ensemble Strategy============")
|
|
254
|
+
# for ensemble model, it's necessary to feed the last state
|
|
255
|
+
# of the previous model to the current model as the initial state
|
|
256
|
+
last_state_ensemble = []
|
|
257
|
+
|
|
258
|
+
ppo_sharpe_list = []
|
|
259
|
+
ddpg_sharpe_list = []
|
|
260
|
+
a2c_sharpe_list = []
|
|
261
|
+
|
|
262
|
+
model_use = []
|
|
263
|
+
validation_start_date_list = []
|
|
264
|
+
validation_end_date_list = []
|
|
265
|
+
iteration_list = []
|
|
266
|
+
|
|
267
|
+
insample_turbulence = self.df[(self.df.date<self.train_period[1]) & (self.df.date>=self.train_period[0])]
|
|
268
|
+
insample_turbulence_threshold = np.quantile(insample_turbulence.turbulence.values, .90)
|
|
269
|
+
|
|
270
|
+
start = time.time()
|
|
271
|
+
for i in range(self.rebalance_window + self.validation_window, len(self.unique_trade_date), self.rebalance_window):
|
|
272
|
+
validation_start_date = self.unique_trade_date[i - self.rebalance_window - self.validation_window]
|
|
273
|
+
validation_end_date = self.unique_trade_date[i - self.rebalance_window]
|
|
274
|
+
|
|
275
|
+
validation_start_date_list.append(validation_start_date)
|
|
276
|
+
validation_end_date_list.append(validation_end_date)
|
|
277
|
+
iteration_list.append(i)
|
|
278
|
+
|
|
279
|
+
print("============================================")
|
|
280
|
+
## initial state is empty
|
|
281
|
+
if i - self.rebalance_window - self.validation_window == 0:
|
|
282
|
+
# inital state
|
|
283
|
+
initial = True
|
|
284
|
+
else:
|
|
285
|
+
# previous state
|
|
286
|
+
initial = False
|
|
287
|
+
|
|
288
|
+
# Tuning trubulence index based on historical data
|
|
289
|
+
# Turbulence lookback window is one quarter (63 days)
|
|
290
|
+
end_date_index = self.df.index[self.df["date"] == self.unique_trade_date[i - self.rebalance_window - self.validation_window]].to_list()[-1]
|
|
291
|
+
start_date_index = end_date_index - 63 + 1
|
|
292
|
+
|
|
293
|
+
historical_turbulence = self.df.iloc[start_date_index:(end_date_index + 1), :]
|
|
294
|
+
|
|
295
|
+
historical_turbulence = historical_turbulence.drop_duplicates(subset=['date'])
|
|
296
|
+
|
|
297
|
+
historical_turbulence_mean = np.mean(historical_turbulence.turbulence.values)
|
|
298
|
+
|
|
299
|
+
print(historical_turbulence_mean)
|
|
300
|
+
|
|
301
|
+
if historical_turbulence_mean > insample_turbulence_threshold:
|
|
302
|
+
# if the mean of the historical data is greater than the 90% quantile of insample turbulence data
|
|
303
|
+
# then we assume that the current market is volatile,
|
|
304
|
+
# therefore we set the 90% quantile of insample turbulence data as the turbulence threshold
|
|
305
|
+
# meaning the current turbulence can't exceed the 90% quantile of insample turbulence data
|
|
306
|
+
turbulence_threshold = insample_turbulence_threshold
|
|
307
|
+
else:
|
|
308
|
+
# if the mean of the historical data is less than the 90% quantile of insample turbulence data
|
|
309
|
+
# then we tune up the turbulence_threshold, meaning we lower the risk
|
|
310
|
+
turbulence_threshold = np.quantile(insample_turbulence.turbulence.values, 1)
|
|
311
|
+
print("turbulence_threshold: ", turbulence_threshold)
|
|
312
|
+
|
|
313
|
+
############## Environment Setup starts ##############
|
|
314
|
+
## training env
|
|
315
|
+
train = data_split(self.df, start=self.train_period[0], end=self.unique_trade_date[i - self.rebalance_window - self.validation_window])
|
|
316
|
+
self.train_env = DummyVecEnv([lambda: StockTradingEnv(train,
|
|
317
|
+
self.stock_dim,
|
|
318
|
+
self.hmax,
|
|
319
|
+
self.initial_amount,
|
|
320
|
+
self.buy_cost_pct,
|
|
321
|
+
self.sell_cost_pct,
|
|
322
|
+
self.reward_scaling,
|
|
323
|
+
self.state_space,
|
|
324
|
+
self.action_space,
|
|
325
|
+
self.tech_indicator_list,
|
|
326
|
+
print_verbosity=self.print_verbosity)])
|
|
327
|
+
|
|
328
|
+
validation = data_split(self.df, start=self.unique_trade_date[i - self.rebalance_window - self.validation_window],
|
|
329
|
+
end=self.unique_trade_date[i - self.rebalance_window])
|
|
330
|
+
############## Environment Setup ends ##############
|
|
331
|
+
|
|
332
|
+
############## Training and Validation starts ##############
|
|
333
|
+
print("======Model training from: ", self.train_period[0], "to ",
|
|
334
|
+
self.unique_trade_date[i - self.rebalance_window - self.validation_window])
|
|
335
|
+
# print("training: ",len(data_split(df, start=20090000, end=test.datadate.unique()[i-rebalance_window]) ))
|
|
336
|
+
# print("==============Model Training===========")
|
|
337
|
+
print("======A2C Training========")
|
|
338
|
+
model_a2c = self.get_model("a2c",self.train_env,policy="MlpPolicy",model_kwargs=A2C_model_kwargs)
|
|
339
|
+
model_a2c = self.train_model(model_a2c, "a2c", tb_log_name="a2c_{}".format(i), iter_num = i, total_timesteps=timesteps_dict['a2c']) #100_000
|
|
340
|
+
|
|
341
|
+
print("======A2C Validation from: ", validation_start_date, "to ",validation_end_date)
|
|
342
|
+
val_env_a2c = DummyVecEnv([lambda: StockTradingEnv(validation,
|
|
343
|
+
self.stock_dim,
|
|
344
|
+
self.hmax,
|
|
345
|
+
self.initial_amount,
|
|
346
|
+
self.buy_cost_pct,
|
|
347
|
+
self.sell_cost_pct,
|
|
348
|
+
self.reward_scaling,
|
|
349
|
+
self.state_space,
|
|
350
|
+
self.action_space,
|
|
351
|
+
self.tech_indicator_list,
|
|
352
|
+
turbulence_threshold=turbulence_threshold,
|
|
353
|
+
iteration=i,
|
|
354
|
+
model_name='A2C',
|
|
355
|
+
mode='validation',
|
|
356
|
+
print_verbosity=self.print_verbosity)])
|
|
357
|
+
val_obs_a2c = val_env_a2c.reset()
|
|
358
|
+
self.DRL_validation(model=model_a2c,test_data=validation,test_env=val_env_a2c,test_obs=val_obs_a2c)
|
|
359
|
+
sharpe_a2c = self.get_validation_sharpe(i,model_name="A2C")
|
|
360
|
+
print("A2C Sharpe Ratio: ", sharpe_a2c)
|
|
361
|
+
|
|
362
|
+
print("======PPO Training========")
|
|
363
|
+
model_ppo = self.get_model("ppo",self.train_env,policy="MlpPolicy",model_kwargs=PPO_model_kwargs)
|
|
364
|
+
model_ppo = self.train_model(model_ppo, "ppo", tb_log_name="ppo_{}".format(i), iter_num = i, total_timesteps=timesteps_dict['ppo']) #100_000
|
|
365
|
+
print("======PPO Validation from: ", validation_start_date, "to ",validation_end_date)
|
|
366
|
+
val_env_ppo = DummyVecEnv([lambda: StockTradingEnv(validation,
|
|
367
|
+
self.stock_dim,
|
|
368
|
+
self.hmax,
|
|
369
|
+
self.initial_amount,
|
|
370
|
+
self.buy_cost_pct,
|
|
371
|
+
self.sell_cost_pct,
|
|
372
|
+
self.reward_scaling,
|
|
373
|
+
self.state_space,
|
|
374
|
+
self.action_space,
|
|
375
|
+
self.tech_indicator_list,
|
|
376
|
+
turbulence_threshold=turbulence_threshold,
|
|
377
|
+
iteration=i,
|
|
378
|
+
model_name='PPO',
|
|
379
|
+
mode='validation',
|
|
380
|
+
print_verbosity=self.print_verbosity)])
|
|
381
|
+
val_obs_ppo = val_env_ppo.reset()
|
|
382
|
+
self.DRL_validation(model=model_ppo,test_data=validation,test_env=val_env_ppo,test_obs=val_obs_ppo)
|
|
383
|
+
sharpe_ppo = self.get_validation_sharpe(i,model_name="PPO")
|
|
384
|
+
print("PPO Sharpe Ratio: ", sharpe_ppo)
|
|
385
|
+
|
|
386
|
+
print("======DDPG Training========")
|
|
387
|
+
model_ddpg = self.get_model("ddpg",self.train_env,policy="MlpPolicy",model_kwargs=DDPG_model_kwargs)
|
|
388
|
+
model_ddpg = self.train_model(model_ddpg, "ddpg", tb_log_name="ddpg_{}".format(i), iter_num = i, total_timesteps=timesteps_dict['ddpg']) #50_000
|
|
389
|
+
print("======DDPG Validation from: ", validation_start_date, "to ",validation_end_date)
|
|
390
|
+
val_env_ddpg = DummyVecEnv([lambda: StockTradingEnv(validation,
|
|
391
|
+
self.stock_dim,
|
|
392
|
+
self.hmax,
|
|
393
|
+
self.initial_amount,
|
|
394
|
+
self.buy_cost_pct,
|
|
395
|
+
self.sell_cost_pct,
|
|
396
|
+
self.reward_scaling,
|
|
397
|
+
self.state_space,
|
|
398
|
+
self.action_space,
|
|
399
|
+
self.tech_indicator_list,
|
|
400
|
+
turbulence_threshold=turbulence_threshold,
|
|
401
|
+
iteration=i,
|
|
402
|
+
model_name='DDPG',
|
|
403
|
+
mode='validation',
|
|
404
|
+
print_verbosity=self.print_verbosity)])
|
|
405
|
+
val_obs_ddpg = val_env_ddpg.reset()
|
|
406
|
+
self.DRL_validation(model=model_ddpg,test_data=validation,test_env=val_env_ddpg,test_obs=val_obs_ddpg)
|
|
407
|
+
sharpe_ddpg = self.get_validation_sharpe(i,model_name="DDPG")
|
|
408
|
+
|
|
409
|
+
ppo_sharpe_list.append(sharpe_ppo)
|
|
410
|
+
a2c_sharpe_list.append(sharpe_a2c)
|
|
411
|
+
ddpg_sharpe_list.append(sharpe_ddpg)
|
|
412
|
+
|
|
413
|
+
print("======Best Model Retraining from: ", self.train_period[0], "to ",
|
|
414
|
+
self.unique_trade_date[i - self.rebalance_window])
|
|
415
|
+
# Environment setup for model retraining up to first trade date
|
|
416
|
+
train_full = data_split(self.df, start=self.train_period[0], end=self.unique_trade_date[i - self.rebalance_window])
|
|
417
|
+
self.train_full_env = DummyVecEnv([lambda: StockTradingEnv(train_full,
|
|
418
|
+
self.stock_dim,
|
|
419
|
+
self.hmax,
|
|
420
|
+
self.initial_amount,
|
|
421
|
+
self.buy_cost_pct,
|
|
422
|
+
self.sell_cost_pct,
|
|
423
|
+
self.reward_scaling,
|
|
424
|
+
self.state_space,
|
|
425
|
+
self.action_space,
|
|
426
|
+
self.tech_indicator_list,
|
|
427
|
+
print_verbosity=self.print_verbosity)])
|
|
428
|
+
# Model Selection based on sharpe ratio
|
|
429
|
+
if (sharpe_ppo >= sharpe_a2c) & (sharpe_ppo >= sharpe_ddpg):
|
|
430
|
+
model_use.append('PPO')
|
|
431
|
+
|
|
432
|
+
model_ensemble = self.get_model("ppo",self.train_full_env,policy="MlpPolicy",model_kwargs=PPO_model_kwargs)
|
|
433
|
+
model_ensemble = self.train_model(model_ensemble, "ensemble", tb_log_name="ensemble_{}".format(i), iter_num = i, total_timesteps=timesteps_dict['ppo']) #100_000
|
|
434
|
+
elif (sharpe_a2c > sharpe_ppo) & (sharpe_a2c > sharpe_ddpg):
|
|
435
|
+
model_use.append('A2C')
|
|
436
|
+
|
|
437
|
+
model_ensemble = self.get_model("a2c",self.train_full_env,policy="MlpPolicy",model_kwargs=A2C_model_kwargs)
|
|
438
|
+
model_ensemble = self.train_model(model_ensemble, "ensemble", tb_log_name="ensemble_{}".format(i), iter_num = i, total_timesteps=timesteps_dict['a2c']) #100_000
|
|
439
|
+
else:
|
|
440
|
+
model_use.append('DDPG')
|
|
441
|
+
|
|
442
|
+
model_ensemble = self.get_model("ddpg",self.train_full_env,policy="MlpPolicy",model_kwargs=DDPG_model_kwargs)
|
|
443
|
+
model_ensemble = self.train_model(model_ensemble, "ensemble", tb_log_name="ensemble_{}".format(i), iter_num = i, total_timesteps=timesteps_dict['ddpg']) #50_000
|
|
444
|
+
|
|
445
|
+
############## Training and Validation ends ##############
|
|
446
|
+
|
|
447
|
+
############## Trading starts ##############
|
|
448
|
+
print("======Trading from: ", self.unique_trade_date[i - self.rebalance_window], "to ", self.unique_trade_date[i])
|
|
449
|
+
#print("Used Model: ", model_ensemble)
|
|
450
|
+
last_state_ensemble = self.DRL_prediction(model=model_ensemble, name="ensemble",
|
|
451
|
+
last_state=last_state_ensemble, iter_num=i,
|
|
452
|
+
turbulence_threshold = turbulence_threshold,
|
|
453
|
+
initial=initial)
|
|
454
|
+
############## Trading ends ##############
|
|
455
|
+
|
|
456
|
+
end = time.time()
|
|
457
|
+
print("Ensemble Strategy took: ", (end - start) / 60, " minutes")
|
|
458
|
+
|
|
459
|
+
df_summary = pd.DataFrame([iteration_list,validation_start_date_list,validation_end_date_list,model_use,a2c_sharpe_list,ppo_sharpe_list,ddpg_sharpe_list]).T
|
|
460
|
+
df_summary.columns = ['Iter','Val Start','Val End','Model Used','A2C Sharpe','PPO Sharpe','DDPG Sharpe']
|
|
461
|
+
|
|
462
|
+
return df_summary
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import division, absolute_import, print_function
|
|
2
|
+
import datetime
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_dataset(*, file_name: str) -> pd.DataFrame:
|
|
8
|
+
"""
|
|
9
|
+
load csv dataset from path
|
|
10
|
+
:return: (df) pandas dataframe
|
|
11
|
+
"""
|
|
12
|
+
# _data = pd.read_csv(f"{config.DATASET_DIR}/{file_name}")
|
|
13
|
+
_data = pd.read_csv(file_name)
|
|
14
|
+
return _data
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def data_split(df, start, end):
|
|
18
|
+
"""
|
|
19
|
+
split the dataset into training or testing using date
|
|
20
|
+
:param data: (df) pandas dataframe, start, end
|
|
21
|
+
:return: (df) pandas dataframe
|
|
22
|
+
"""
|
|
23
|
+
data = df[(df.date >= start) & (df.date < end)]
|
|
24
|
+
data = data.sort_values(["date", "tic"], ignore_index=True)
|
|
25
|
+
data.index = data.date.factorize()[0]
|
|
26
|
+
return data
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def convert_to_datetime(time):
|
|
30
|
+
time_fmt = "%Y-%m-%dT%H:%M:%S"
|
|
31
|
+
if isinstance(time, str):
|
|
32
|
+
return datetime.datetime.strptime(time, time_fmt)
|