diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2098c80 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ + +# Table of contents +* [Install](#install) +* [Technologies](#technologies) +* [How to run](#how-to-run) +* [Use custom model](#use-custom-model) + +# Install + +```console +pip install ejtraderRL -U +``` +# Install from source +```console +git clone https://github.com/ejtraderLabs/ejtraderRL.git +cd trade-rl +pip install . +``` + +# Technologies +| Technologies | version | +| -- | -- | +| python | >= 3.7 | +| tensorflow | >= 2.7.0 | +| numpy |>= 1.21.4 | +| pandas |>= 1.3.4 | +| ta | >= 0.7.0 | + +# How to run + +```python +from ejtraderRL import data, agent + +# forex data +df = data.get_forex_data("EURUSD", "h1") +# stoch data +#df = data.get_stock_data("AAPL") + +agent = agent.DQN(df=df, model_name="efficientnet_b0", lr=1e-4, pip_scale=25, n=3, use_device="cpu", + gamma=0.99, train_spread=0.2, balance=1000, spread=10, risk=0.01) + + +""" +:param df: pandas dataframe or csv file. Must contain open, low, high, close +:param lr: learning rate +:param model_name: None or model name, If None -> model is not created. +:param pip_scale: Controls the degree of overfitting +:param n: int +:param use_device: tpu or gpu or cpu +:param gamma: float +:param train_spread: Determine the degree of long-term training. The smaller the value, the more short-term the trade. +:param balance: Account size +:param spread: Cost of Trade +:param risk: What percentage of the balance is at risk +""" + +agent.train() +``` + +# Use custom model +```python +from tensorflow.keras import layers, optimizers +from ejtraderRL import nn, agent, data + +# forex data +df = data.get_forex_data("EURUSD", "h1") +# stoch data +df = data.get_stock_data("AAPL") + +agent = agent.DQN(df=df, model_name=None, lr=1e-4, pip_scale=25, n=3, use_device="cpu", + gamma=0.99, train_spread=0.2, spread=0.7, balance=1000 risk=0.1) + +def custom_model(): + dim = 32 + noise = layers.Dropout + noise_r = 0.1 + + inputs, x = nn.layers.inputs_f(agent.x.shape[1:], dim, 5, 1, False, "same", noise, noise_r) + x = nn.block.ConvBlock(dim, "conv1d", "resnet", 1, True, None, noise, noise_r)(x) + out = nn.layers.DQNOutput(2, None, noise, noise_r)(x) + + model = nn.model.Model(inputs, x) + model.compile(optimizers.Adam(agent.lr, clipnorm=1.), nn.losses.DQNLoss) + + return model + +agent._build_model = custom_model +agent.build_model() +``` diff --git a/build/lib/ejtraderRL/__init__.py b/build/lib/ejtraderRL/__init__.py new file mode 100644 index 0000000..978a0e7 --- /dev/null +++ b/build/lib/ejtraderRL/__init__.py @@ -0,0 +1,4 @@ +from ejtraderRL import agent +from ejtraderRL import nn +from ejtraderRL import data + diff --git a/build/lib/ejtraderRL/agent/__init__.py b/build/lib/ejtraderRL/agent/__init__.py new file mode 100644 index 0000000..b9244e5 --- /dev/null +++ b/build/lib/ejtraderRL/agent/__init__.py @@ -0,0 +1,2 @@ +from .dqn import * +from .qrdqn import * \ No newline at end of file diff --git a/build/lib/ejtraderRL/agent/dqn.py b/build/lib/ejtraderRL/agent/dqn.py new file mode 100644 index 0000000..99e376d --- /dev/null +++ b/build/lib/ejtraderRL/agent/dqn.py @@ -0,0 +1,410 @@ +import warnings + +import matplotlib.pyplot as plt +import numpy as np +import tensorflow as tf +import pandas as pd +from IPython.display import clear_output +from ejtraderRL import nn +import ta + +warnings.simplefilter('ignore') + + +class DQN: + agent_name = "dqn" + loss = nn.losses.DQNLoss + + def __init__(self, df: pd.DataFrame, model_name, lr=1e-4, pip_scale=25, n=3, use_device="cpu", + gamma=0.99, train_spread=0.2, spread=10, balance=1000, risk=0.01): + """ + :param df: pandas dataframe or csv file. Must contain open, low, high, close + :param lr: learning rate + :param model_name: None or model name, If None -> model is not created. + :param pip_scale: Controls the degree of overfitting + :param n: int + :param use_device: tpu or gpu or cpu + :param gamma: float + :param train_spread: Determine the degree of long-term training. The smaller the value, the more short-term the trade. + :param spread: Cost of Trade + :param balance: Account size + :param risk: What percentage of the balance is at risk + """ + + self.df = df + self.model_name = model_name + self.lr = lr + self.pip_scale = pip_scale + self.n = n + self.use_device = use_device.lower() + self.gamma = gamma + self.train_spread = train_spread + self.spread = spread + self.risk = risk + + self.actions = {0: 1, 1: -1} + + if self.use_device == "tpu": + try: + resolver = tf.distribute.cluster_resolver.TPUClusterResolver(tpu='') + tf.config.experimental_connect_to_cluster(resolver) + tf.tpu.experimental.initialize_tpu_system(resolver) + self.strategy = tf.distribute.TPUStrategy(resolver) + except: + self.use_device = "cpu" + + self.train_rewards, self.test_rewards = [], [] + self.max_profits, self.max_pips = [], [] + self.train_loss, self.val_loss = [], [] + self.test_pip, self.test_profit = [], [] + + self.x, self.y, self.atr = self.env() + self.train_step = np.arange(int(len(self.y) * 0.9)) + self.test_step = np.arange(self.train_step[-1], len(self.y)) + + if self.model_name: + self.model, self.target_model = self.build_model() + + self.states, self.new_states, self.returns = self.train_data() + self.ind = np.arange(len(self.returns)) + np.random.shuffle(self.ind) + + self.max_profit, self.max_pip = 0, 0 + self.now_max_profit, self.now_max_pip = 0, 0 + + self.account_size = balance + + def env(self): + if isinstance(self.df, str): + self.df = pd.read_csv(self.df) + + self.df.columns = self.df.columns.str.lower() + + self.df["sig"] = self.df.close - self.df.close.shift(1) + self.df["atr"] = ta.volatility.average_true_range(self.df.high, self.df.low, self.df.close) + self.df = self.df.dropna() + + x = [] + y = [] + atr = [] + + window_size = 30 + for i in range(window_size, len(self.df.close)): + x.append(self.df.sig[i - window_size:i]) + y.append(self.df.close[i - 1]) + atr.append(self.df.atr[i - 1]) + + x = np.array(x, np.float32).reshape((-1, 30, 1)) + y = np.array(y, np.int32).reshape((-1,)) + atr = np.array(atr, np.int32).reshape((-1,)) + + return x, y, atr + + def _build_model(self) -> nn.model.Model: + model = nn.build_model(self.model_name, self.x.shape[1:], 2, None, self.agent_name) + model.compile( + tf.keras.optimizers.Adam(self.lr, clipnorm=1.), loss=self.loss(), steps_per_execution=100 + ) + return model + + def build_model(self): + if self.use_device == "tpu": + with self.strategy.scope(): + model = self._build_model() + target_model = tf.keras.models.clone_model(model) + else: + model = self._build_model() + target_model = tf.keras.models.clone_model(model) + target_model.set_weights(model.get_weights()) + + return model, target_model + + def train_data(self): + h, h_ = 0, self.train_step[-1] + n = self.n + + states = self.x[h:h_ - n].copy() + close = self.y[h:h_] + + buy = np.array([close[i + n] - close[i] for i in range(len(close) - n)]).reshape((-1,)) + scale = np.quantile(abs(buy), 0.99) + buy = np.clip(buy / scale, -1, 1) * self.pip_scale + sell = -buy + + spread = self.train_spread * self.pip_scale + + returns = np.zeros((len(close) - n, 2, 2)) + returns[:, 0, 0] = buy + returns[:, 0, 1] = sell - spread + returns[:, 1, 0] = buy - spread + returns[:, 1, 1] = sell + + new_states = np.roll(states, -n, axis=0)[:-n] + states = states[:-n] + returns = returns[:-n] + + return states, new_states, returns + + def get_actions(self, df): + q = self.model.predict(df, 102800, workers=10000, use_multiprocessing=True) + actions = np.argmax(q, -1) + a = np.argmax([q[0, 0, 1], q[0, 1, 0]]) + act = [a] + + for i in range(1, len(actions)): + a = actions[i, a] + act.append(a) + + return np.array(act).reshape((-1,)) + + def trade(self, h, h_): + df = self.x[h:h_] + trend = self.y[h:h_] + atr = self.atr[h:h_] + profit = self.account_size + + actions = self.get_actions(df) + + old_a = actions[0] + old_price = trend[0] + + total_pip, total_profit = 0, 0 + self.now_max_profit, self.now_max_pip = 0, 0 + pips, profits = [], [] + total_pips, total_profits = [], [] + buy, sell = [], [] + + if old_a == 0: + buy.append(0) + else: + sell.append(0) + + loss_cut = -atr[0] * 2 + position_size = int((profit * self.risk) / -loss_cut) + position_size = np.minimum(position_size, 500 * 200 * 100) + position_size = np.maximum(position_size, 1) + + for i, (act, price, atr) in enumerate(zip(actions, trend, atr)): + if old_a != act: + old_a = self.actions[old_a] + pip = (price - old_price) * old_a + total_pip += pip - self.spread + pips.append(pip - self.spread) + total_pips.append(total_pip) + + gain = pip * position_size - self.spread * position_size + total_profit += gain + profits.append(gain) + total_profits.append(total_profit) + + self.now_max_pip = np.maximum(self.now_max_pip, total_pip) + self.now_max_profit = np.maximum(self.now_max_profit, total_profit) + + old_price = price + old_a = act + + loss_cut = -atr * 2 + position_size = int((profit * self.risk) / -loss_cut) + position_size = np.minimum(position_size, 500 * 200 * 100) + position_size = np.maximum(position_size, 0) + + if act == 0: + buy.append(i) + else: + sell.append(i) + + pips = np.array(pips) + + return pips, profits, total_pips, total_profits, total_pip, total_profit, buy, sell + + def evolute(self, h, h_): + pips, profits, total_pips, total_profits, total_pip, total_profit, buy, sell = self.trade(h, h_) + + acc = np.mean(pips > 0) + total_win = np.sum(pips[pips > 0]) + total_lose = np.sum(pips[pips < 0]) + rr = total_win / abs(total_lose) + ev = (np.mean(pips[pips > 0]) * acc + np.mean(pips[pips < 0]) * (1 - acc)) / abs(np.mean(pips[pips < 0])) + + plt.figure(figsize=(10, 5), dpi=100) + plt.subplot(1, 2, 1) + plt.plot(total_pips) + plt.subplot(1, 2, 2) + plt.plot(total_profits) + plt.show() + + print( + f"acc = {acc}, pips = {sum(pips)}\n" + f"total_win = {total_win}, total_lose = {total_lose}\n" + f"rr = {rr}, ev = {ev}\n" + ) + + def plot_trade(self, train=False, test=False, period=1): + assert train or test + h = 0 + if test: + h = self.test_step[0] + elif train: + h = np.random.randint(0, int(self.train_step[-1] - 960 * period)) + h_ = h + len(self.train_step) // 12 * period + trend = self.y[h:h_] + + pips, profits, total_pips, total_profits, total_pip, total_profit, buy, sell = self.trade(h, h_) + + plt.figure(figsize=(20, 10), dpi=100) + plt.plot(trend, color="g", alpha=1, label="close") + plt.plot(trend, "^", markevery=buy, c="red", label='buy', alpha=0.7) + plt.plot(trend, "v", markevery=sell, c="blue", label='sell', alpha=0.7) + + plt.legend() + plt.show() + + print(f"pip = {np.sum(pips)}" + f"\naccount size = {total_profit}" + f"\ngrowth rate = {total_profit / self.account_size}" + f"\naccuracy = {np.mean(np.array(pips) > 0)}") + + def plot_result(self, w, risk=0.1): + self.model.set_weights(w) + self.risk = risk + + plt.figure(figsize=(20, 5), dpi=100) + plt.subplot(1, 2, 1) + plt.plot(self.test_pip) + plt.subplot(1, 2, 2) + plt.plot(self.test_profit) + plt.show() + + ################################################################################ + self.plot_trade(train=False, test=True, period=9) + ################################################################################ + plt.figure(figsize=(10, 5)) + plt.plot(self.train_loss) + plt.plot(self.val_loss) + plt.title('Model loss') + plt.ylabel('Loss') + plt.xlabel('Epoch') + plt.legend(['Train', 'Validation'], loc='upper left') + plt.show() + + len_ = len(self.train_loss) // 2 + plt.figure(figsize=(10, 5)) + plt.plot(self.train_loss[len_:]) + plt.plot(self.val_loss[len_:]) + plt.title('Model loss') + plt.ylabel('Loss') + plt.xlabel('Epoch') + plt.legend(['Train', 'Validation'], loc='upper left') + plt.show() + ################################################################################ + self.evolute(self.test_step[0], self.test_step[-1]) + ################################################################################ + plt.figure(figsize=(20, 5), dpi=100) + plt.subplot(1, 2, 1) + plt.plot(self.train_rewards) + plt.subplot(1, 2, 2) + plt.plot(self.test_rewards) + plt.show() + + print(f"profits = {self.max_profit}, max profits = {self.max_profits}\n" + f"pips = {self.max_pip}, max pip = {self.max_pips}") + ################################################################################ + self.evolute(self.test_step[0] - len(self.train_step), self.test_step[0]) + + def target_q(self, returns, target_q, target_a): + if self.train_loss: + target_a = np.argmax(target_a, -1) + rr = range(len(returns)) + returns[:, 0, 0] += self.gamma * target_q[rr, 0, target_a[rr, 0]] + returns[:, 0, 1] += self.gamma * target_q[rr, 1, target_a[rr, 1]] + returns[:, 1, 0] += self.gamma * target_q[rr, 0, target_a[rr, 0]] + returns[:, 1, 1] += self.gamma * target_q[rr, 1, target_a[rr, 1]] + + assert np.mean(np.isnan(returns) == False) == 1 + + return returns + + def _train(self, epoch, batch_size): + ind = self.ind + + states, new_states, returns = self.states[ind].copy(), self.new_states[ind].copy(), self.returns[ind].copy() + + if self.train_loss: + target_q = self.target_model.predict(new_states, 102800) + else: + target_q = np.zeros((len(returns), 2, 2), np.float32) + + for _ in range(epoch): + returns = self.returns[ind].copy() + noise = np.random.normal(0, 0.1, states.shape) + + target_a = self.model.predict(new_states + noise, 102800) + returns = self.target_q(returns, target_q, target_a) + + h = self.model.fit(states + noise, returns, batch_size, validation_split=0.2) + self.train_loss.extend(h.history["loss"]) + self.val_loss.extend(h.history["val_loss"]) + + p = 12 + + if len(self.train_loss) >= 200: + + pips, profits, total_pips, total_profits, total_pip, total_profit, buy, sell = \ + self.trade(self.test_step[0] - len(self.test_step), self.test_step[0]) + self.train_rewards.append(np.sum(pips)) + pips, profits, total_pips, total_profits, total_pip, total_profit, buy, sell = \ + self.trade(self.test_step[0], self.test_step[-1]) + self.test_rewards.append(np.sum(pips)) + + acc = np.mean(pips > 0) + + total_win = np.sum(pips[pips > 0]) + total_lose = np.sum(pips[pips < 0]) + ev = \ + (np.mean(pips[pips > 0]) * acc + np.mean(pips[pips < 0]) * (1 - acc)) / abs(np.mean(pips[pips < 0])) + ev = np.clip(ev, 0, 0.75) / 0.75 + rr = np.clip(total_win / abs(total_lose), 0, 2.5) / 2.5 + acc /= 0.7 + + self.max_pip = (rr + ev + acc) * np.clip(np.sum(profits) / self.account_size, 1, None) + self.max_pip = 0 if np.isnan(self.max_pip) else self.max_pip + + self.max_profit /= self.account_size + + self.test_pip.append(self.max_pip) + self.test_profit.append(self.max_profit) + + if len(pips) >= (p * 5): + if self.max_pips <= self.max_pip: + self.best_w = self.model.get_weights() + self.max_profits = self.max_profit + + self.max_profits = np.maximum(self.max_profit, self.max_profits) + self.max_pips = np.maximum(self.max_pip, self.max_pips) + + plt.figure(figsize=(20, 5), dpi=100) + plt.subplot(1, 2, 1) + plt.plot(self.train_rewards) + plt.subplot(1, 2, 2) + plt.plot(self.test_rewards) + plt.show() + + print(f"profits = {self.max_profit}, max profits = {self.max_profits}\n" + f"pips = {self.max_pip}, max pip = {self.max_pips}") + + def train(self, epoch=40, batch_size=2056): + for _ in range(600 // epoch): + clear_output() + plt.figure(figsize=(10, 5)) + plt.plot(self.train_loss) + plt.plot(self.val_loss) + plt.title('Model loss') + plt.ylabel('Loss') + plt.xlabel('Epoch') + plt.legend(['Train', 'Validation'], loc='upper left') + plt.show() + self._train(epoch, batch_size) + self.target_model.set_weights(self.model.get_weights()) + + +__all__ = ["DQN"] diff --git a/build/lib/ejtraderRL/agent/qrdqn.py b/build/lib/ejtraderRL/agent/qrdqn.py new file mode 100644 index 0000000..b73481d --- /dev/null +++ b/build/lib/ejtraderRL/agent/qrdqn.py @@ -0,0 +1,48 @@ +import warnings + +import numpy as np + +from ejtraderRL import nn +from ejtraderRL.agent import DQN + +warnings.simplefilter('ignore') + + +class QRDQN(DQN): + agent_name = "QRDQN" + agent_loss = nn.losses.QRDQNLoss + + def get_actions(self, df): + q = self.model.predict(df, 10280, workers=10000, use_multiprocessing=True) + q = np.mean(q, 2) + actions = np.argmax(q, -1) + a = np.argmax([q[0, 0, 1], q[0, 1, 0]]) + act = [a] + + for i in range(1, len(actions)): + a = actions[i, a] + act.append(a) + + return act + + def target_q(self, returns, target_q, target_a): + returns = np.reshape(returns, (-1, 2, 1, 2)) + returns = np.tile(returns, (1, 1, 32, 1)) + + if self.train_loss and target_q.shape == returns.shape: + target_a = np.argmax(np.mean(target_a, axis=2), -1) + rr = range(len(returns)) + returns[:, 0, :, 0] += self.gamma * target_q[rr, 0, :, target_a[:, 0]] + returns[:, 0, :, 1] += self.gamma * target_q[rr, 1, :, target_a[:, 1]] + returns[:, 1, :, 0] += self.gamma * target_q[rr, 0, :, target_a[:, 0]] + returns[:, 1, :, 1] += self.gamma * target_q[rr, 1, :, target_a[:, 1]] + + assert np.mean(np.isnan(returns) == False) == 1 + + return returns + + def train(self, epoch=50, batch_size=2056): + super(QRDQN, self).train(epoch, batch_size) + + +__all__ = ["QRDQN"] \ No newline at end of file diff --git a/build/lib/ejtraderRL/data/__init__.py b/build/lib/ejtraderRL/data/__init__.py new file mode 100644 index 0000000..01acc7c --- /dev/null +++ b/build/lib/ejtraderRL/data/__init__.py @@ -0,0 +1 @@ +from .get_data import * \ No newline at end of file diff --git a/build/lib/ejtraderRL/data/get_data.py b/build/lib/ejtraderRL/data/get_data.py new file mode 100644 index 0000000..a7141f4 --- /dev/null +++ b/build/lib/ejtraderRL/data/get_data.py @@ -0,0 +1,23 @@ +import pandas as pd +import pandas_datareader as pdr + + +def get_forex_data(symbol: str, timeframe: str): + """ + :param symbol: AUDJPY, AUDUSD, EURCHF, EURGBP, EURJPY, EURUSD, GBPJPY, GBPUSD, USDCAD, USDCHF, USDJPY, XAUUSD + :param timeframe: m15, m30, h1, h4, d1 + :return: pandas DataFrame + """ + symbol = symbol.upper() + + url = "https://raw.githubusercontent.com/komo135/forex-historical-data/main/" + url += symbol + "/" + symbol + timeframe.lower() + ".csv" + + return pd.read_csv(url) + + +def get_stock_data(symbol, start=None, end=None): + return pdr.get_data_yahoo(symbol, strt=start, end=end) + + +__all__ = ["get_stock_data", "get_forex_data"] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/__init__.py b/build/lib/ejtraderRL/nn/__init__.py new file mode 100644 index 0000000..509ca3b --- /dev/null +++ b/build/lib/ejtraderRL/nn/__init__.py @@ -0,0 +1,5 @@ +from . import layers +from . import model +from . import block +from . import losses +from .build_model import * diff --git a/build/lib/ejtraderRL/nn/block/__init__.py b/build/lib/ejtraderRL/nn/block/__init__.py new file mode 100644 index 0000000..cd3ba4d --- /dev/null +++ b/build/lib/ejtraderRL/nn/block/__init__.py @@ -0,0 +1,3 @@ +from .conv import * +from .convnext import * +from .efficient import * diff --git a/build/lib/ejtraderRL/nn/block/conv.py b/build/lib/ejtraderRL/nn/block/conv.py new file mode 100644 index 0000000..edf98a0 --- /dev/null +++ b/build/lib/ejtraderRL/nn/block/conv.py @@ -0,0 +1,78 @@ +import tensorflow as tf +from tensorflow.keras import layers +from ejtraderRL.nn.layers import layer, Activation +import numpy as np + + +class ConvBlock(layers.Layer): + def __init__(self, dim, layer_name="Conv1D", types="resnet", + groups=1, bias=True, attention=None, noise=layers.Dropout, noise_r=0, **kwargs): + """ + :param dim: output dimention + :param layer_name: layer name + :param types: "densenet" or "resnet" + """ + super(ConvBlock, self).__init__() + + self.dim = dim + self.layer_name = layer_name + self.types = types.lower() + self.groups = groups + self.bias = bias + self.attention = attention + self.noise = noise + self.noise_r = noise_r + + assert self.types == "densenet" or self.types == "resnet" + + if self.types == "resnet": + self.l = [ + Activation(), + noise(noise_r), + layer(layer_name, dim, bias, 7, groups, **kwargs), + Activation(), + layer("conv1d", dim * 4, True, 1, 1, **kwargs), + Activation(), + layer(attention, dim * 4, **kwargs), + noise(noise_r), + layer("conv1d", dim, True, 1, 1, **kwargs), + ] + else: + self.l = [ + Activation(), + noise(noise_r), + layer("conv1d", dim * 4, True, 1, 1, **kwargs), + Activation(), + layer(attention, dim * 4, **kwargs), + noise(noise_r), + layer(layer_name, dim, bias, 7, groups), + ] + + self.l = np.array(self.l) + self.l = list(self.l[self.l != None].reshape((-1,))) + + def call(self, inputs, *args, **kwargs): + x = inputs + for l in self.l: + x = l(x) + + if self.types == "densenet": + return tf.concat([inputs, x], axis=-1) + elif self.types == "resnet": + return tf.add(inputs, x) + + def get_config(self): + config = { + "dim": self.dim, + "layer_name": self.layer_name, + "types": self.types, + "groups": self.groups, + "bias": self.bias, + "attention": self.attention, + "noise": self.noise, + "noise_r": self.noise_r + } + return config + + +__all__ = ["ConvBlock"] diff --git a/build/lib/ejtraderRL/nn/block/convnext.py b/build/lib/ejtraderRL/nn/block/convnext.py new file mode 100644 index 0000000..7d3cecd --- /dev/null +++ b/build/lib/ejtraderRL/nn/block/convnext.py @@ -0,0 +1,52 @@ +import numpy as np +import tensorflow as tf +from tensorflow.keras import layers + +from ejtraderRL.nn.layers import layer + + +class ConvnextBlock(layers.Layer): + def __init__(self, dim: int, layer_name: str, types: str, attention=None, noise=layers.Dropout, noise_r=0, + **kwargs): + super(ConvnextBlock, self).__init__() + self.dim = dim + self.layer_name = layer_name + self.types = types + self.attention = attention + + self.l = [ + noise(noise_r), + layer(layer_name, dim, True, 7), + layers.LayerNormalization(), + layer("conv1d", dim * 4, True, 1, 1, **kwargs), + layers.Activation("gelu"), + layer(attention, dim * 4), + noise(noise_r), + layer("conv1d", dim, True, 1, 1, **kwargs), + ] + self.l = np.array(self.l) + self.l = list(self.l[self.l != None].reshape((-1,))) + + def call(self, inputs, *args, **kwargs): + x = inputs + for l in self.l: + x = l(x) + + if self.types == "resnet": + return inputs + x + elif self.types == "densenet": + return tf.concat([inputs, x], -1) + + def get_config(self): + config = { + "dim": self.dim, + "layer_name": self.layer_name, + "types": self.types, + "attention": self.attention, + "noise": self.noise, + "noise_r": self.noise_r + } + return config + + +__all__ = ["ConvnextBlock"] diff --git a/build/lib/ejtraderRL/nn/block/efficient.py b/build/lib/ejtraderRL/nn/block/efficient.py new file mode 100644 index 0000000..e77a738 --- /dev/null +++ b/build/lib/ejtraderRL/nn/block/efficient.py @@ -0,0 +1,109 @@ +import numpy as np +import tensorflow as tf +from tensorflow.keras import layers + +from ejtraderRL.nn.layers import SE, Activation, layer + + +class MBBlock(layers.Layer): + def __init__(self, idim, odim, expand_ratio, kernel_size, + se_ratio=0.25, layer_name="DepthwiseConv1D", types="resnet", noise=layers.Dropout, noise_r=0, **kwargs): + super(MBBlock, self).__init__() + self.idim = idim + self.odim = odim + self.expand_ratio = expand_ratio + self.se_ratio = se_ratio + self.kernel_size = kernel_size + self.layer_name = layer_name.lower() + self.types = types.lower() + self.noise = noise + self.noise_r = noise_r + assert self.types == "resnet" or self.types == "densenet" + + self.l = np.array([ + Activation(), + noise(noise_r), + layer("conv1d", int(idim * expand_ratio), False, 1, 1), + Activation(), + layer(layer_name, int(idim * expand_ratio), False, kernel_size, 1), + Activation(), + SE(int(idim * expand_ratio), se_ratio) if se_ratio > 0 else None, + noise(noise_r), + layer("conv1d", odim, False, 1, 1) + ]) + self.l = list(self.l[self.l is not None].reshape((-1,))) + + def call(self, inputs, *args, **kwargs): + x = inputs + for l in self.l: + x = l(x) + + if self.types == "resnet": + return x + inputs + elif self.types == "densenet": + return tf.concat([inputs, x], axis=-1) + + def get_config(self): + config = { + "idim": self.idim, + "odim": self.odim, + "expand_ratio": self.expand_ratio, + "se_ratio": self.se_ratio, + "kernel_size": self.kernel_size, + "layer_name": self.layer_name, + "types": self.types, + "noise": self.noise, + "noise_r": self.noise_r + } + return config + + +class FuseBlock(layers.Layer): + def __init__(self, idim, odim, expand_ratio, kernel_size, + se_ratio=0.25, layer_name="DepthwiseConv1D", types="resnet", noise=layers.Dropout, noise_r=0, **kwargs): + super(FuseBlock, self).__init__() + self.idim = idim + self.odim = odim + self.expand_ratio = expand_ratio + self.se_ratio = se_ratio + self.kernel_size = kernel_size + self.layer_name = layer_name.lower() + self.types = types.lower() + + self.l = np.array([ + Activation("mish"), + noise(noise_r), + layer("conv1d", int(idim * expand_ratio), False, 1, 1), + SE(int(idim * expand_ratio), se_ratio) if se_ratio > 0 else None, + Activation("mish"), + noise(noise_r), + layer("conv1d", odim, False, 1, 1), + ]) + self.l = list(self.l[self.l != None].reshape((-1,))) + + def call(self, inputs, *args, **kwargs): + x = inputs + for l in self.l: + x = l(x) + + if self.types == "resnet": + return x + inputs + elif self.types == "densenet": + return tf.concat([inputs, x], axis=-1) + + def get_config(self): + config = { + "idim": self.idim, + "odim": self.odim, + "expand_ratio": self.expand_ratio, + "se_ratio": self.se_ratio, + "kernel_size": self.kernel_size, + "layer_name": self.layer_name, + "types": self.types, + "noise": self.noise, + "noise_r": self.noise_r + } + return config + + +__all__ = ["MBBlock", "FuseBlock"] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/build_model.py b/build/lib/ejtraderRL/nn/build_model.py new file mode 100644 index 0000000..5da32f6 --- /dev/null +++ b/build/lib/ejtraderRL/nn/build_model.py @@ -0,0 +1,207 @@ +import numpy as np +import tensorflow as tf +from tensorflow.keras.layers import GaussianDropout, Conv1D, LayerNormalization + +from .block import ConvBlock, ConvnextBlock, FuseBlock, MBBlock +from .layers import Activation, inputs_f, Output, DQNOutput, QRDQNOutput +from .model import * + +output_dict = {None: Output, "dqn": DQNOutput, "qrdqn": QRDQNOutput} + +gamma = [1, 1.2, 1.4, 1.7, 2., 2.4, 2.9, 3.5] +alpha = [1, 1.1, 1.21, 1.33, 1.46, 1.61, 1.77, 1.94] + +l_b0 = (3, 4, 6, 3) + +noise = GaussianDropout +noise_b0 = [0.1, 0.1, 0.1] +noise_b1 = [0.1, 0.1, 0.1] +noise_b2 = [0.1, 0.1, 0.1] +noise_b3 = [0.1, 0.1, 0.1] +noise_b4 = [0.1, 0.1, 0.1] +noise_b5 = [0.1, 0.1, 0.1] +noise_b6 = [0.1, 0.1, 0.1] +noise_b7 = [0.1, 0.1, 0.1] +noise_l = [noise_b0, noise_b1, noise_b2, noise_b3, noise_b4, noise_b5, noise_b6, noise_b7] + + +class BuildModel: + def __init__(self, num_layer, dim: int, layer_name: str, types: str, scale=0, + groups=1, ej=False, se=False, cbam=False, vit=False, + efficientv1=False, efficientv2=False, + convnext=False): + self.num_layer = num_layer + self.layer_name = layer_name.lower() + self.types = types + self.groups = groups + self.ej = bool(ej) + self.se = bool(se) + self.cbam = bool(cbam) + self.efficientv1 = bool(efficientv1) + self.efficientv2 = bool(efficientv2) + self.vit = vit + self.convnext = convnext + self.attn = "se" if se else "cbam" if cbam else None + + self.noise_ratio = noise_l[scale] + self.gamma = gamma[scale] + self.alpha = alpha[scale] + self.dim = int(dim * self.gamma) if dim else dim + self.num_layer = num_layer if num_layer else np.round(np.array(l_b0) * self.alpha).astype(int) + + if efficientv1 or efficientv2: + self.dim = int((16 if self.types == "resnet" else 32) * self.gamma) + if self.dim and self.layer_name == "lambdalayer": + self.dim = 4 * int(np.round(self.dim / 4)) + + def build_eff_block(self, l): + block = None + if self.efficientv1: + block = [MBBlock for _ in range(len(l))] + elif self.efficientv2: + block = [FuseBlock, FuseBlock, FuseBlock] + block.extend([MBBlock for _ in range(len(l) - 3)]) + + self.block = block + + def transition(self, x, dim=None, pool=True): + if self.types == "densenet": + dim = x.shape[-1] // 2 if pool else x.shape[-1] + elif self.types == "resnet": + dim = self.dim = self.dim * 2 if dim is None else dim + + if self.convnext: + x = LayerNormalization()(x) + x = Conv1D(dim, 2, 1, "same", kernel_initializer="he_normal")(x) + if pool: + x = tf.keras.layers.AvgPool1D()(x) + x = LayerNormalization()(x) + else: + x = Activation()(x) + x = Conv1D(dim, 1, 1, "same", kernel_initializer="he_normal")(x) + if pool: + x = tf.keras.layers.AvgPool1D()(x) + + return x + + def efficient_model(self, x): + l = [1, 2, 2, 3, 3, 4, 1] + k = [3, 3, 5, 3, 5, 5, 3] + pool = [False, False, True, True, True, False, True] + self.build_eff_block(l) + + if self.types == "resnet": + ic = [16, 16, 24, 40, 80, 112, 192] + oc = [16, 24, 40, 80, 112, 192, 320] + ep = [1, 6, 6, 6, 6, 6, 6] + else: + ic = [32 for _ in range(len(l))] + oc = ic + ep = [6 for _ in range(len(l))] + + ic = (np.array(ic) * self.gamma).astype(np.int32) + oc = (np.array(oc) * self.gamma).astype(np.int32) + l = np.round(np.array(l) * self.alpha).astype(np.int32) + + if self.layer_name == "lambdalayer": + ic = [int(4 * np.round(ic / 4)) for ic in ic] + oc = [int(4 * np.round(oc / 4)) for oc in oc] + + for e, (ic, oc, ep, l, k, pool, block) in enumerate(zip(ic, oc, ep, l, k, pool, self.block)): + + if e != 0: + x = self.transition(x, oc, pool) + + for _ in range(l): + x = block(ic, oc, ep, k, 0.25, self.layer_name, self.types, noise, self.noise_ratio[1])(x) + + return x + + def conv_model(self, x): + + for i, l in enumerate(self.num_layer): + if i != 0: + x = self.transition(x, None, True) + + for _ in range(l): + if self.convnext: + x = ConvnextBlock(self.dim, self.layer_name, self.types, self.attn, noise, self.noise_ratio[1])(x) + else: + x = ConvBlock(self.dim, self.layer_name, self.types, self.groups, True, self.attn, noise, + self.noise_ratio[1])(x) + + return x + + def build_model(self, input_shape, output_size, output_activation, agent=None): + inputs, x = inputs_f(input_shape, self.dim, 5, 1, False, "same", noise, self.noise_ratio[0]) + + if self.efficientv1 or self.efficientv2: + x = self.efficient_model(x) + else: + x = self.conv_model(x) + + x = tf.keras.layers.GlobalAvgPool1D()(x) + + x = output_dict[agent](output_size, output_activation, noise, self.noise_ratio[2])(x) + + return EJModel(inputs, x) if self.ej else Model(inputs, x) + + +network_dict = {} +available_network = [] + + +def create_network(name, num_layer, dim, layer_name, types, **kwargs): + available_network.append(f"{name}") + for i in range(8): + network_dict.update({f"{name}_b{i}": lambda i=i: BuildModel(num_layer, dim, layer_name, types, i, **kwargs)}) + + +create_network("efficientnet", [], 0, "DepthwiseConv1D", "resnet", efficientv1=True) +create_network("ej_efficientnet", [], 0, "DepthwiseConv1D", "resnet", efficientv1=True, ej=True) + +create_network("dense_efficientnet", [], 0, "DepthwiseConv1D", "densenet", efficientv1=True) +create_network("ej_dense_efficientnet", [], 0, "DepthwiseConv1D", "densenet", efficientv1=True, ej=True) + +create_network("lambda_efficientnet", [], 0, "lambdalayer", "resnet", efficientv1=True) +create_network("ej_lambda_efficientnet", [], 0, "lambdalayer", "resnet", efficientv1=True, ej=True) + +create_network("efficientnetv2", [], 0, "DepthwiseConv1D", "resnet", efficientv2=True) +create_network("ej_efficientnetv2", [], 0, "DepthwiseConv1D", "resnet", efficientv2=True, ej=True) + +create_network("resnet", [], 48, "Conv1D", "resnet") +create_network("ej_resnet", [], 48, "Conv1D", "resnet", ej=True) +create_network("se_resnet", [], 48, "Conv1D", "resnet", se=True) +create_network("ej_se_resnet", [], 48, "Conv1D", "resnet", se=True, ej=True) + +create_network("densenet", [], 48, "Conv1D", "densenet") +create_network("ej_densenet", [], 48, "Conv1D", "densenet", ej=True) +create_network("se_densenet", [], 48, "Conv1D", "densenet", se=True) +create_network("ej_se_densenet", [], 48, "Conv1D", "densenet", se=True, ej=True) + +create_network("lambda_resnet", [], 48, "LambdaLayer", "resnet") +create_network("ej_lambda_resnet", [], 48, "LambdaLayer", "resnet", ej=True) +create_network("se_lambda_resnet", [], 48, "LambdaLayer", "resnet", se=True) +create_network("ej_se_lambda_resnet", [], 48, "LambdaLayer", "resnet", se=True, ej=True) + +create_network("convnext", [], 48, "DepthwiseConv1D", "resnet", convnext=True) +create_network("ej_convnext", [], 48, "DepthwiseConv1D", "resnet", convnext=True, ej=True) +create_network("se_convnext", [], 48, "DepthwiseConv1D", "resnet", convnext=True, se=True) +create_network("ej_se_convnext", [], 48, "DepthwiseConv1D", "resnet", convnext=True, se=True, ej=True) + +create_network("lambda_convnext", [], 48, "LambdaLayer", "resnet", convnext=True) +create_network("ej_lambda_convnext", [], 48, "LambdaLayer", "resnet", convnext=True, ej=True) +create_network("se_lambda_convnext", [], 48, "LambdaLayer", "resnet", convnext=True, se=True) +create_network("ej_se_lambda_convnext", [], 48, "LambdaLayer", "resnet", convnext=True, se=True, ej=True) + + +def build_model(model_name: str, input_shape: tuple, output_size: int, output_activation=None, agent=None) -> Model: + model = network_dict[model_name]() + model = model.build_model(input_shape, output_size, output_activation, agent) + + return model + + +available_network = np.array(available_network).reshape((-1,)) + +__all__ = ["build_model", "available_network", "network_dict", "BuildModel"] diff --git a/build/lib/ejtraderRL/nn/layers/__init__.py b/build/lib/ejtraderRL/nn/layers/__init__.py new file mode 100644 index 0000000..e8da03d --- /dev/null +++ b/build/lib/ejtraderRL/nn/layers/__init__.py @@ -0,0 +1,26 @@ +from .core import * +from .activation import * +from .attention import * +from .transformer import * +from .pyconv import * +from .skconv import * +from .lambdalayer import * +from .depthwiseconv1d import * +from tensorflow.keras.layers import Conv1D + + +def layer(layer_name: str, dim, use_bias=True, kernel_size=1, groups=1, **kwargs): + if layer_name: + layer_name = layer_name.lower() + if layer_name == "conv1d": + return Conv1D(dim, kernel_size, 1, "same", kernel_initializer="he_normal", use_bias=use_bias, groups=groups, **kwargs) + elif layer_name == "lambdalayer": + return LambdaLayer(dim, **kwargs) + elif layer_name == "skconv": + return SKConv(dim, groups, **kwargs) + elif layer_name == "depthwiseconv1d": + return DepthwiseConv1D(kernel_size, 1, "same", kernel_initializer="he_normal", use_bias=use_bias) + elif layer_name == "se": + return SE(dim, **kwargs) + elif layer_name == "cbam": + return CBAM(dim, **kwargs) diff --git a/build/lib/ejtraderRL/nn/layers/activation.py b/build/lib/ejtraderRL/nn/layers/activation.py new file mode 100644 index 0000000..a1b5873 --- /dev/null +++ b/build/lib/ejtraderRL/nn/layers/activation.py @@ -0,0 +1,35 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf + + +class Mish(layers.Layer): + def call(self, inputs, *args, **kwargs): + return inputs * tf.tanh(tf.math.softplus(inputs)) + + +class Activation(layers.Layer): + def __init__(self, activation="swish", normalization=layers.LayerNormalization): + super(Activation, self).__init__() + self.activation = activation.lower() + self.normalization = normalization + if self.activation == "mish": + self.act = Mish() + else: + self.act = layers.Activation(activation) + if normalization: + self.norm = normalization() + + def call(self, inputs, training=None, mask=None): + if self.normalization: + x = self.norm(inputs) + else: + x = inputs + return self.act(x) + + def get_config(self): + return {"activation": self.activation, "normalization": self.normalization} + + +__all__ = [ + "Activation" +] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/layers/attention.py b/build/lib/ejtraderRL/nn/layers/attention.py new file mode 100644 index 0000000..b9cdfc9 --- /dev/null +++ b/build/lib/ejtraderRL/nn/layers/attention.py @@ -0,0 +1,70 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf + + +class SE(layers.Layer): + def __init__(self, dim, r=.0625): + super(SE, self).__init__() + self.dim = dim + self.r = r + + self.mlp = Sequential([ + layers.Dense(int(dim * r), "relu", kernel_initializer="he_normal"), + layers.Dense(dim, "sigmoid") + ]) + + def call(self, inputs, *args, **kwargs): + x = tf.reduce_mean(inputs, axis=1, keepdims=True) + x = self.mlp(x) + x *= inputs + + return x + + def get_config(self): + config = { + "dim": self.dim, + "r": self.r + } + + return config + + +class CBAM(layers.Layer): + def __init__(self, filters, r=.0625): + super(CBAM, self).__init__() + self.filters = filters + self.r = r + + self.avg_pool = layers.GlobalAvgPool1D() + self.max_pool = layers.GlobalMaxPool1D() + self.mlp = [ + layers.Dense(int(filters * r), "relu", kernel_initializer="he_normal"), + layers.Dense(filters) + ] + + self.concat = layers.Concatenate() + self.conv = layers.Conv1D(1, 7, 1, "same", activation="sigmoid") + + def compute_mlp(self, x, pool): + x = pool(x) + for mlp in self.mlp: + x = mlp(x) + + return x + + def call(self, inputs, training=None, *args, **kwargs): + x = self.compute_mlp(inputs, self.avg_pool) + self.compute_mlp(inputs, self.max_pool) + x = inputs * tf.reshape(tf.nn.sigmoid(x), (-1, 1, self.filters)) + + conv = self.concat([tf.reduce_mean(x, -1, keepdims=True), tf.reduce_max(x, -1, keepdims=True)]) + return x * self.conv(conv) + + def get_config(self): + config = {"filters": self.filters, "r": self.r} + return config + + +__all__ = [ + "SE", + "CBAM" +] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/layers/core.py b/build/lib/ejtraderRL/nn/layers/core.py new file mode 100644 index 0000000..8da8661 --- /dev/null +++ b/build/lib/ejtraderRL/nn/layers/core.py @@ -0,0 +1,74 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf + + +def inputs_f(input_shape, dim, kernel_size, strides, pooling, padding="same", noise=layers.Dropout, noise_r=0): + inputs = tf.keras.layers.Input(input_shape) + x = noise(noise_r)(inputs) + x = layers.Conv1D(dim, kernel_size, strides, padding, kernel_initializer="he_normal")(x) + if pooling: + x = tf.keras.layers.AvgPool1D(3, 2)(x) + + return inputs, x + + +class Output(layers.Layer): + def __init__(self, output_size, activation=None, noise=layers.Dropout, noise_r=0): + super(Output, self).__init__() + self.output_size = output_size + self.activation = activation + self.noise = noise + self.noise_r = noise_r + self.out = Sequential([ + noise(noise_r), + layers.Dense(output_size, activation) + ]) + + def call(self, inputs, *args, **kwargs): + return self.out(inputs) + + def get_config(self): + return {"output_size": self.output_size, + "activation": self.activation, + "noise": self.noise, + "noise_r":self.noise_r} + + +class DQNOutput(Output): + def __init__(self, output_size, activation=None, noise=layers.Dropout, noise_r=0): + super(DQNOutput, self).__init__(output_size, activation, noise, noise_r) + + self.out = [ + [noise(noise_r), layers.Dense(output_size), layers.Reshape((output_size, 1))] + for _ in range(output_size) + ] + + def call(self, inputs, *args, **kwargs): + out = [] + for l1 in self.out: + q = inputs + for l2 in l1: + q = l2(q) + out.append(q) + + return tf.concat(out, axis=-1) + + +class QRDQNOutput(DQNOutput): + def __init__(self, output_size, activation=None, noise=layers.Dropout, noise_r=0, quantile_size=32): + super(QRDQNOutput, self).__init__(output_size, activation, noise, noise_r) + self.quantile_size = quantile_size + + self.out = [ + [noise(noise_r), layers.Dense(output_size * quantile_size), layers.Reshape((output_size, quantile_size, 1))] + for _ in range(output_size) + ] + + def get_config(self): + config = super(QRDQNOutput, self).get_config() + config.update({"quantile_size": self.quantile_size}) + + return config + + +__all__ = ["inputs_f", "Output", "DQNOutput", "QRDQNOutput"] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/layers/depthwiseconv1d.py b/build/lib/ejtraderRL/nn/layers/depthwiseconv1d.py new file mode 100644 index 0000000..449b40e --- /dev/null +++ b/build/lib/ejtraderRL/nn/layers/depthwiseconv1d.py @@ -0,0 +1,237 @@ +from tensorflow.python.framework import tensor_shape +from tensorflow.python.keras import constraints +from tensorflow.python.keras import initializers +from tensorflow.python.keras import regularizers +from tensorflow.python.keras.engine.input_spec import InputSpec +from tensorflow.python.keras.layers.convolutional import Conv1D +# imports for backwards namespace compatibility +# pylint: disable=unused-import +# pylint: enable=unused-import +from tensorflow.python.keras.utils import conv_utils +from tensorflow.python.ops import array_ops +from tensorflow.python.ops import nn + + +# pylint: disable=g-classes-have-attributes + + +class DepthwiseConv1D(Conv1D): + """Depthwise 1D convolution. + + Depthwise convolution is a type of convolution in which a single convolutional + filter is apply to each input channel (i.e. in a depthwise way). + You can understand depthwise convolution as being + the first step in a depthwise separable convolution. + + It is implemented via the following steps: + + - Split the input into individual channels. + - Convolve each input with the layer's kernel (called a depthwise kernel). + - Stack the convolved outputs together (along the channels axis). + + Unlike a regular 1D convolution, depthwise convolution does not mix + information across different input channels. + + The `depth_multiplier` argument controls how many + output channels are generated per input channel in the depthwise step. + + Args: + kernel_size: An integer or tuple/list of 2 integers, specifying the + height and width of the 1D convolution window. + Can be a single integer to specify the same value for + all spatial dimensions. + strides: An integer or tuple/list of 2 integers, + specifying the strides of the convolution along the height and width. + Can be a single integer to specify the same value for + all spatial dimensions. + Specifying any stride value != 1 is incompatible with specifying + any `dilation_rate` value != 1. + padding: one of `'valid'` or `'same'` (case-insensitive). + `"valid"` means no padding. `"same"` results in padding with zeros evenly + to the left/right or up/down of the input such that output has the same + height/width dimension as the input. + depth_multiplier: The number of depthwise convolution output channels + for each input channel. + The total number of depthwise convolution output + channels will be equal to `filters_in * depth_multiplier`. + data_format: A string, + one of `channels_last` (default) or `channels_first`. + The ordering of the dimensions in the inputs. + `channels_last` corresponds to inputs with shape + `(batch_size, height, width, channels)` while `channels_first` + corresponds to inputs with shape + `(batch_size, channels, height, width)`. + It defaults to the `image_data_format` value found in your + Keras config file at `~/.keras/keras.json`. + If you never set it, then it will be 'channels_last'. + dilation_rate: An integer or tuple/list of 2 integers, specifying + the dilation rate to use for dilated convolution. + Currently, specifying any `dilation_rate` value != 1 is + incompatible with specifying any `strides` value != 1. + activation: Activation function to use. + If you don't specify anything, no activation is applied ( + see `keras.activations`). + use_bias: Boolean, whether the layer uses a bias vector. + depthwise_initializer: Initializer for the depthwise kernel matrix ( + see `keras.initializers`). If None, the default initializer ( + 'glorot_uniform') will be used. + bias_initializer: Initializer for the bias vector ( + see `keras.initializers`). If None, the default initializer ( + 'zeros') will bs used. + depthwise_regularizer: Regularizer function applied to + the depthwise kernel matrix (see `keras.regularizers`). + bias_regularizer: Regularizer function applied to the bias vector ( + see `keras.regularizers`). + activity_regularizer: Regularizer function applied to + the output of the layer (its 'activation') ( + see `keras.regularizers`). + depthwise_constraint: Constraint function applied to + the depthwise kernel matrix ( + see `keras.constraints`). + bias_constraint: Constraint function applied to the bias vector ( + see `keras.constraints`). + + Input shape: + 3D tensor with shape: + `(batch_size, channels, steps)` if data_format='channels_first' + or 3D tensor with shape: + `(batch_size, steps, channels)` if data_format='channels_last'. + + Output shape: + 3D tensor with shape: + `(batch_size, filters, new_steps)` if data_format='channels_first' + or 3D tensor with shape: + `(batch_size, new_steps, filters)` if data_format='channels_last'. + `new_steps` value might have changed due to padding or strides. + + Returns: + A tensor of rank 3 representing + `activation(separableconv1d(inputs, kernel) + bias)`. + + Raises: + ValueError: when both `strides` > 1 and `dilation_rate` > 1. + """ + + def __init__(self, + kernel_size, + strides=1, + padding='valid', + depth_multiplier=1, + data_format=None, + dilation_rate=1, + activation=None, + use_bias=True, + depthwise_initializer='glorot_uniform', + bias_initializer='zeros', + depthwise_regularizer=None, + bias_regularizer=None, + activity_regularizer=None, + depthwise_constraint=None, + bias_constraint=None, + **kwargs): + super(DepthwiseConv1D, self).__init__( + filters=None, + kernel_size=kernel_size, + strides=strides, + padding=padding, + data_format=data_format, + dilation_rate=dilation_rate, + activation=activation, + use_bias=use_bias, + bias_regularizer=bias_regularizer, + activity_regularizer=activity_regularizer, + bias_constraint=bias_constraint, + **kwargs) + self.depth_multiplier = depth_multiplier + self.depthwise_initializer = initializers.get(depthwise_initializer) + self.depthwise_regularizer = regularizers.get(depthwise_regularizer) + self.depthwise_constraint = constraints.get(depthwise_constraint) + self.bias_initializer = initializers.get(bias_initializer) + + def build(self, input_shape): + input_shape = tensor_shape.TensorShape(input_shape) + channel_axis = self._get_channel_axis() + if input_shape.dims[channel_axis].value is None: + raise ValueError('The channel dimension of the inputs ' + 'should be defined. Found `None`.') + input_dim = int(input_shape[channel_axis]) + self.input_spec = InputSpec(ndim=self.rank + 2, + axes={channel_axis: input_dim}) + depthwise_kernel_shape = self.kernel_size + (input_dim, + self.depth_multiplier) + + self.depthwise_kernel = self.add_weight( + shape=depthwise_kernel_shape, + initializer=self.depthwise_initializer, + name='depthwise_kernel', + regularizer=self.depthwise_regularizer, + constraint=self.depthwise_constraint) + + if self.use_bias: + self.bias = self.add_weight(shape=(input_dim * self.depth_multiplier,), + initializer=self.bias_initializer, + name='bias', + regularizer=self.bias_regularizer, + constraint=self.bias_constraint) + else: + self.bias = None + self.built = True + + def call(self, inputs): + if self.padding == 'causal': + inputs = array_ops.pad(inputs, self._compute_causal_padding(inputs)) + if self.data_format == 'channels_last': + strides = (1,) + self.strides * 2 + (1,) + spatial_start_dim = 1 + else: + strides = (1, 1) + self.strides * 2 + spatial_start_dim = 2 + + # Explicitly broadcast inputs and kernels to 4D. + inputs = array_ops.expand_dims(inputs, spatial_start_dim) + depthwise_kernel = array_ops.expand_dims(self.depthwise_kernel, 0) + dilation_rate = (1,) + self.dilation_rate + + if self.padding == 'causal': + op_padding = 'valid' + else: + op_padding = self.padding + op_padding = op_padding.upper() + + outputs = nn.depthwise_conv2d( + inputs, + depthwise_kernel, + strides=strides, + padding=op_padding, + rate=dilation_rate, + data_format=conv_utils.convert_data_format(self.data_format, ndim=4)) + + if self.use_bias: + outputs = nn.bias_add( + outputs, + self.bias, + data_format=conv_utils.convert_data_format(self.data_format, ndim=4)) + + outputs = array_ops.squeeze(outputs, [spatial_start_dim]) + + if self.activation is not None: + return self.activation(outputs) + return outputs + + def get_config(self): + config = super(DepthwiseConv1D, self).get_config() + config.pop('filters') + config.pop('kernel_initializer') + config.pop('kernel_regularizer') + config.pop('kernel_constraint') + config['depth_multiplier'] = self.depth_multiplier + config['depthwise_initializer'] = initializers.serialize( + self.depthwise_initializer) + config['depthwise_regularizer'] = regularizers.serialize( + self.depthwise_regularizer) + config['depthwise_constraint'] = constraints.serialize( + self.depthwise_constraint) + return config + + +__all__ = ["DepthwiseConv1D"] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/layers/lambdalayer.py b/build/lib/ejtraderRL/nn/layers/lambdalayer.py new file mode 100644 index 0000000..4992c24 --- /dev/null +++ b/build/lib/ejtraderRL/nn/layers/lambdalayer.py @@ -0,0 +1,83 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf + + +class LambdaLayer(layers.Layer): + + def __init__(self, out_dim, heads=4, use_bias=False, u=4, kernel_size=7, **kwargs): + super(LambdaLayer, self).__init__() + + self.out_dim = out_dim + k = 16 + self.heads = heads + self.v = out_dim // heads + self.u = u + self.kernel_size = kernel_size + self.use_bias = use_bias + + self.top_q = tf.keras.layers.Conv1D(k * heads, 1, 1, "same", use_bias=use_bias) + self.top_k = tf.keras.layers.Conv1D(k * u, 1, 1, "same", use_bias=use_bias) + self.top_v = tf.keras.layers.Conv1D(self.v * self.u, 1, 1, "same", use_bias=use_bias) + + self.norm_q = tf.keras.layers.LayerNormalization() + self.norm_v = tf.keras.layers.LayerNormalization() + + self.rearrange_q = Sequential([ + layers.Reshape((-1, heads, k)), + layers.Permute((2, 3, 1)) + ]) + self.rearrange_k = Sequential([ + layers.Reshape((-1, u, k)), + layers.Permute((2, 3, 1)) + ]) + self.rearrange_v = Sequential([ + layers.Reshape((-1, u, self.v)), + layers.Permute((2, 3, 1)) + ]) + + self.rearrange_v2 = layers.Permute((2, 3, 1)) + self.rearrange_lp = layers.Permute((1, 3, 2)) + self.rearrange_output = layers.Reshape((-1, out_dim)) + + self.pos_conv = tf.keras.layers.Conv2D(k, (1, self.kernel_size), padding="same") + + def call(self, inputs, *args, **kwargs): + q = self.top_q(inputs) + k = self.top_k(inputs) + v = self.top_v(inputs) + + q = self.norm_q(q) + v = self.norm_v(v) + + q = self.rearrange_q(q) + k = self.rearrange_k(k) + v = self.rearrange_v(v) + + k = tf.nn.softmax(k) + + lc = tf.einsum("b u k n, b u v n -> b k v", k, v) + yc = tf.einsum("b h k n, b k v -> b n h v", q, lc) + + v = self.rearrange_v2(v) + lp = self.pos_conv(v) + lp = self.rearrange_lp(lp) + yp = tf.einsum("b h k n, b v k n -> b n h v", q, lp) + + y = yc + yp + output = self.rearrange_output(y) + + return output + + def get_config(self): + config = { + "out_dim": self.out_dim, + "heads": self.heads, + "use_bias": self.use_bias, + "u": self.u, + "kernel_size": self.kernel_size + } + + return config + + +__all__ = ["LambdaLayer"] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/layers/pyconv.py b/build/lib/ejtraderRL/nn/layers/pyconv.py new file mode 100644 index 0000000..8e3eeb3 --- /dev/null +++ b/build/lib/ejtraderRL/nn/layers/pyconv.py @@ -0,0 +1,34 @@ +from tensorflow.keras import layers + + +class Pyconv(layers.Layer): + def __init__(self, dim, groups=32): + super(Pyconv, self).__init__() + + self.dim = dim + self.groups = groups + + self.k = k = [3, 5, 7, 9] + self.conv = [ + layers.Conv1D(dim // 4, k, 1, "same", kernel_initializer="he_normal", groups=groups) for k + in k + ] + self.concat = layers.Concatenate() + + def call(self, inputs, *args, **kwargs): + x = [] + for conv in self.conv: + x.append(conv(inputs)) + + return self.concat(x) + + def get_config(self): + config = { + "dim": self.dim, + "groups": self.groups + } + + return config + + +__all__ = ["Pyconv"] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/layers/skconv.py b/build/lib/ejtraderRL/nn/layers/skconv.py new file mode 100644 index 0000000..0cf863a --- /dev/null +++ b/build/lib/ejtraderRL/nn/layers/skconv.py @@ -0,0 +1,53 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf +import numpy as np + + +class SKConv(layers.Layer): + def __init__(self, filters: int, groups=32, r=16, **kwargs): + super(SKConv, self).__init__() + self.filters = filters + self.z_filters = np.maximum(filters // r, 32) + self.groups = groups + self.r = r + + self.u1 = layers.Conv1D(filters, 3, 1, "same", kernel_initializer="he_normal", groups=groups) + self.u2 = layers.Conv1D(filters, 5, 1, "same", kernel_initializer="he_normal", dilation_rate=1, groups=groups) + + self.add = layers.Add(axis=-1) + + self.z = layers.Dense(self.z_filters, "elu", kernel_initializer="he_normal") + + self.a = layers.Dense(self.filters) + self.b = layers.Dense(self.filters) + self.concat = layers.Concatenate(axis=1) + + def call(self, inputs, *args, **kwargs): + u1 = self.u1(inputs) + u2 = self.u2(inputs) + + u = self.add([u1, u2]) + s = tf.reduce_mean(u, axis=1, keepdims=True) + z = self.z(s) + a = self.a(z) + b = self.b(z) + ab = self.concat([a, b]) + ab = tf.nn.softmax(ab, axis=1) + a, b = tf.split(ab, 2, 1) + + u1 *= a + u2 *= b + + return u1 + u2 + + def get_config(self): + config = { + "filters": self.filters, + "groups": self.groups, + "r": self.r + } + + return config + + +__all__ = ["SKConv"] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/layers/transformer.py b/build/lib/ejtraderRL/nn/layers/transformer.py new file mode 100644 index 0000000..d4b09f6 --- /dev/null +++ b/build/lib/ejtraderRL/nn/layers/transformer.py @@ -0,0 +1,132 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf + + +class PositionAdd(layers.Layer): + def build(self, input_shape): + self.pe = self.add_weight("pe", [input_shape[1], input_shape[2]], + initializer=tf.keras.initializers.zeros()) + + def call(self, inputs, **kwargs): + return inputs + self.pe + + +class MHSA(layers.Layer): + def __init__(self, dim, num_heads, use_bias=True, noise=layers.Dropout, noise_r=0, **kwargs): + super(MHSA, self).__init__(**kwargs) + self.num_heads = num_heads + self.dim = dim + self.noise = noise + self.noise_r = noise_r + self.use_bias = use_bias + + head_dim = dim // num_heads + self.scale = head_dim ** -0.5 + + self.qkv = layers.Dense(dim * 3, use_bias=use_bias) + self.qkv_reshape = layers.Reshape((-1, num_heads, head_dim)) + self.qv_permute = layers.Permute((2, 1, 3)) + self.k_permute = layers.Permute((2, 3, 1)) + self.attn_reshape = Sequential([ + layers.Permute((2, 1, 3)), + layers.Reshape((-1, dim)) + ]) + self.proj = layers.Dense(dim) + self.drop_out = noise(noise_r) + + def call(self, inputs: tf.Tensor, training=False, *args, **kwargs): + qkv = self.qkv(inputs) + q, k, v = tf.split(qkv, 3, -1) + + q = self.qkv_reshape(q) + k = self.qkv_reshape(k) + v = self.qkv_reshape(v) + + q = self.qv_permute(q) + k = self.k_permute(k) + v = self.qv_permute(v) + + attn = tf.matmul(q, k) * self.scale + attn = tf.nn.softmax(attn, axis=-1) + + x = tf.matmul(attn, v) + x = self.attn_reshape(x) + x = self.proj(x) + x = self.drop_out(x) + + return x + + def get_config(self): + config = { + "num_heads": self.num_heads, + "dim": self.dim, + "use_bias": self.use_bias, + "noise": self.noise, + "noise_r": self.noise_r + } + return config + + +class TransformerMlp(layers.Layer): + def __init__(self, dim, mlp_dim, noise=layers.Dropout, noise_r=0): + super(TransformerMlp, self).__init__() + self.dim = dim + self.mlp_dim = mlp_dim + self.noise = noise + self.noise_r = noise_r + + self.dense = Sequential([ + layers.Dense(mlp_dim, "gelu"), + noise(noise_r), + layers.Dense(dim), + noise(noise_r) + ]) + + def call(self, inputs, *args, **kwargs): + return self.dense(inputs) + + def get_config(self): + config = { + "dim": self.dim, + "mlp_dim": self.mlp_dim, + "noise": self.noise, + "noise_r": self.noise_r + } + + return config + + +class Transformer(layers.Layer): + def __init__(self, dim, mlp_dim, heads, use_bias=False, noise=layers.Dropout, noise_r=0): + super(Transformer, self).__init__() + self.dim = dim + self.mlp_dim = mlp_dim + self.heads = heads + self.use_bias = use_bias + + self.attn = Sequential([layers.LayerNormalization(), MHSA(dim, heads, use_bias, noise, noise_r)]) + self.mlp = Sequential([layers.LayerNormalization(), TransformerMlp(dim, mlp_dim, noise, noise_r)]) + + def call(self, inputs, *args, **kwargs): + x = self.attn(inputs) + inputs + x = self.mlp(x) + x + + return x + + def get_config(self): + config = { + "dim": self.dim, + "mlp_dim": self.mlp_dim, + "heads": self.heads, + "use_bias": self.use_bias + } + + return config + + +__all__ = [ + "PositionAdd", + "MHSA", + "TransformerMlp", + "Transformer" +] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/losses/__init__.py b/build/lib/ejtraderRL/nn/losses/__init__.py new file mode 100644 index 0000000..9455de6 --- /dev/null +++ b/build/lib/ejtraderRL/nn/losses/__init__.py @@ -0,0 +1 @@ +from .losses import * diff --git a/build/lib/ejtraderRL/nn/losses/losses.py b/build/lib/ejtraderRL/nn/losses/losses.py new file mode 100644 index 0000000..2ef6a47 --- /dev/null +++ b/build/lib/ejtraderRL/nn/losses/losses.py @@ -0,0 +1,44 @@ +from tensorflow.keras.losses import Loss +import tensorflow as tf +import numpy as np + + +class DQNLoss(Loss): + k = 2 + + def call(self, q_backup, q): + k = self.k + + error = q_backup - q + loss = tf.where(tf.abs(error) <= k, error ** 2 * 0.5, 0.5 * k ** 2 + k * (tf.abs(error) - k)) + loss = tf.reduce_mean(loss) + + return loss + + +class QRDQNLoss(Loss): + def __init__(self, quantile_size=32): + super(QRDQNLoss, self).__init__() + self.quantile_size = quantile_size + + self.k = 1 + plus_tau = np.arange(1, quantile_size, dtype=np.float32) / quantile_size + self.plus_tau = np.reshape(plus_tau, (1, 1, 32, 1)) + self.minus_tau = np.abs(self.plus_tau - 1) + + def call(self, q_backup, q): + k = self.k + + error = q_backup - q + loss = tf.where(tf.abs(error) <= k, error ** 2 * 0.5, 0.5 * k ** 2 + k * (tf.abs(error) - k)) + loss = tf.where(error > 0, loss * self.plus_tau, loss * self.minus_tau) + loss = tf.reduce_mean(loss, (0, 1, 3)) + loss = tf.reduce_sum(loss) + + return loss + + def get_config(self): + return {"quantile_size": self.quantile_size} + + +__all__ = ["DQNLoss", "QRDQNLoss"] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/model/__init__.py b/build/lib/ejtraderRL/nn/model/__init__.py new file mode 100644 index 0000000..8fefd2b --- /dev/null +++ b/build/lib/ejtraderRL/nn/model/__init__.py @@ -0,0 +1 @@ +from .ej import * diff --git a/build/lib/ejtraderRL/nn/model/ej.py b/build/lib/ejtraderRL/nn/model/ej.py new file mode 100644 index 0000000..37fb566 --- /dev/null +++ b/build/lib/ejtraderRL/nn/model/ej.py @@ -0,0 +1,62 @@ +from tensorflow.keras import Model +import tensorflow as tf + + +class EJModel(Model): + rho = 0.05 + + def train_step(self, data): + x, y = data + e_ws = [] + + with tf.GradientTape() as tape: + predictions = self(x, training=True) + loss = self.compiled_loss(y, predictions, regularization_losses=self.losses) + + if "_optimizer" in dir(self.optimizer): # mixed float policy + grads_and_vars = self.optimizer._optimizer._compute_gradients(loss, var_list=self.trainable_variables, + tape=tape) + else: + grads_and_vars = self.optimizer._compute_gradients(loss, var_list=self.trainable_variables, tape=tape) + + grads = [g for g, _ in grads_and_vars] + + grad_norm = self._grad_norm(grads) + scale = self.rho / (grad_norm + 1e-12) + + for (grad, param) in zip(grads, self.trainable_variables): + e_w = grad * scale + e_ws.append(e_w) + param.assign_add(e_w) + + with tf.GradientTape() as tape: + predictions = self(x, training=True) + loss = self.compiled_loss(y, predictions, regularization_losses=self.losses) + + if "_optimizer" in dir(self.optimizer): # mixed float policy + grads_and_vars = self.optimizer._optimizer._compute_gradients(loss, var_list=self.trainable_variables, + tape=tape) + else: + grads_and_vars = self.optimizer._compute_gradients(loss, var_list=self.trainable_variables, tape=tape) + grads = [g for g, _ in grads_and_vars] + + for e_w, param in zip(e_ws, self.trainable_variables): + param.assign_sub(e_w) + + grads_and_vars = list(zip(grads, self.trainable_variables)) + + self.optimizer.apply_gradients(grads_and_vars) + self.compiled_metrics.update_state(y, predictions) + + return {m.name: m.result() for m in self.metrics} + + def _grad_norm(self, gradients): + norm = tf.norm( + tf.stack([ + tf.norm(grad) for grad in gradients if grad is not None + ]) + ) + return norm + + +__all__ = ["EJModel", "Model"] \ No newline at end of file diff --git a/build/lib/ejtraderRL/nn/model/sam.py b/build/lib/ejtraderRL/nn/model/sam.py new file mode 100644 index 0000000..37fb566 --- /dev/null +++ b/build/lib/ejtraderRL/nn/model/sam.py @@ -0,0 +1,62 @@ +from tensorflow.keras import Model +import tensorflow as tf + + +class EJModel(Model): + rho = 0.05 + + def train_step(self, data): + x, y = data + e_ws = [] + + with tf.GradientTape() as tape: + predictions = self(x, training=True) + loss = self.compiled_loss(y, predictions, regularization_losses=self.losses) + + if "_optimizer" in dir(self.optimizer): # mixed float policy + grads_and_vars = self.optimizer._optimizer._compute_gradients(loss, var_list=self.trainable_variables, + tape=tape) + else: + grads_and_vars = self.optimizer._compute_gradients(loss, var_list=self.trainable_variables, tape=tape) + + grads = [g for g, _ in grads_and_vars] + + grad_norm = self._grad_norm(grads) + scale = self.rho / (grad_norm + 1e-12) + + for (grad, param) in zip(grads, self.trainable_variables): + e_w = grad * scale + e_ws.append(e_w) + param.assign_add(e_w) + + with tf.GradientTape() as tape: + predictions = self(x, training=True) + loss = self.compiled_loss(y, predictions, regularization_losses=self.losses) + + if "_optimizer" in dir(self.optimizer): # mixed float policy + grads_and_vars = self.optimizer._optimizer._compute_gradients(loss, var_list=self.trainable_variables, + tape=tape) + else: + grads_and_vars = self.optimizer._compute_gradients(loss, var_list=self.trainable_variables, tape=tape) + grads = [g for g, _ in grads_and_vars] + + for e_w, param in zip(e_ws, self.trainable_variables): + param.assign_sub(e_w) + + grads_and_vars = list(zip(grads, self.trainable_variables)) + + self.optimizer.apply_gradients(grads_and_vars) + self.compiled_metrics.update_state(y, predictions) + + return {m.name: m.result() for m in self.metrics} + + def _grad_norm(self, gradients): + norm = tf.norm( + tf.stack([ + tf.norm(grad) for grad in gradients if grad is not None + ]) + ) + return norm + + +__all__ = ["EJModel", "Model"] \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..48e88df --- /dev/null +++ b/deploy.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +rm -rf dist/* +python setup.py sdist bdist_wheel +twine upload dist/* \ No newline at end of file diff --git a/dist/ejtraderRL-1.0.0-py3-none-any.whl b/dist/ejtraderRL-1.0.0-py3-none-any.whl new file mode 100644 index 0000000..5a94c1b Binary files /dev/null and b/dist/ejtraderRL-1.0.0-py3-none-any.whl differ diff --git a/dist/ejtraderRL-1.0.0.tar.gz b/dist/ejtraderRL-1.0.0.tar.gz new file mode 100644 index 0000000..8cd6709 Binary files /dev/null and b/dist/ejtraderRL-1.0.0.tar.gz differ diff --git a/ejtraderRL.egg-info/PKG-INFO b/ejtraderRL.egg-info/PKG-INFO new file mode 100644 index 0000000..06ed720 --- /dev/null +++ b/ejtraderRL.egg-info/PKG-INFO @@ -0,0 +1,104 @@ +Metadata-Version: 2.1 +Name: ejtraderRL +Version: 1.0.0 +Summary: Reinforcement learning Trading envoriments. +Home-page: https://github.com/ejejtraderRLabs/ +Author: traderpedroso +Author-email: info@ejtrader.com +License: Apache-2.0 License +Keywords: ejtraderRL +Platform: UNKNOWN +Description-Content-Type: text/markdown +License-File: LICENSE + + +# Table of contents +* [Install](#install) +* [Technologies](#technologies) +* [How to run](#how-to-run) +* [Use custom model](#use-custom-model) + +# Install + +```console +pip install ejtraderRL -U +``` +# Install from source +```console +git clone https://github.com/ejtraderLabs/ejtraderRL.git +cd trade-rl +pip install . +``` + +# Technologies +| Technologies | version | +| -- | -- | +| python | >= 3.7 | +| tensorflow | >= 2.7.0 | +| numpy |>= 1.21.4 | +| pandas |>= 1.3.4 | +| ta | >= 0.7.0 | + +# How to run + +```python +from ejtraderRL import data, agent + +# forex data +df = data.get_forex_data("EURUSD", "h1") +# stoch data +#df = data.get_stock_data("AAPL") + +agent = agent.DQN(df=df, model_name="efficientnet_b0", lr=1e-4, pip_scale=25, n=3, use_device="cpu", + gamma=0.99, train_spread=0.2, balance=1000, spread=10, risk=0.01) + + +""" +:param df: pandas dataframe or csv file. Must contain open, low, high, close +:param lr: learning rate +:param model_name: None or model name, If None -> model is not created. +:param pip_scale: Controls the degree of overfitting +:param n: int +:param use_device: tpu or gpu or cpu +:param gamma: float +:param train_spread: Determine the degree of long-term training. The smaller the value, the more short-term the trade. +:param balance: Account size +:param spread: Cost of Trade +:param risk: What percentage of the balance is at risk +""" + +agent.train() +``` + +# Use custom model +```python +from tensorflow.keras import layers, optimizers +from ejtraderRL import nn, agent, data + +# forex data +df = data.get_forex_data("EURUSD", "h1") +# stoch data +df = data.get_stock_data("AAPL") + +agent = agent.DQN(df=df, model_name=None, lr=1e-4, pip_scale=25, n=3, use_device="cpu", + gamma=0.99, train_spread=0.2, spread=0.7, balance=1000 risk=0.1) + +def custom_model(): + dim = 32 + noise = layers.Dropout + noise_r = 0.1 + + inputs, x = nn.layers.inputs_f(agent.x.shape[1:], dim, 5, 1, False, "same", noise, noise_r) + x = nn.block.ConvBlock(dim, "conv1d", "resnet", 1, True, None, noise, noise_r)(x) + out = nn.layers.DQNOutput(2, None, noise, noise_r)(x) + + model = nn.model.Model(inputs, x) + model.compile(optimizers.Adam(agent.lr, clipnorm=1.), nn.losses.DQNLoss) + + return model + +agent._build_model = custom_model +agent.build_model() +``` + + diff --git a/ejtraderRL.egg-info/SOURCES.txt b/ejtraderRL.egg-info/SOURCES.txt new file mode 100644 index 0000000..9581227 --- /dev/null +++ b/ejtraderRL.egg-info/SOURCES.txt @@ -0,0 +1,33 @@ +LICENSE +README.md +setup.py +ejtraderRL/__init__.py +ejtraderRL.egg-info/PKG-INFO +ejtraderRL.egg-info/SOURCES.txt +ejtraderRL.egg-info/dependency_links.txt +ejtraderRL.egg-info/requires.txt +ejtraderRL.egg-info/top_level.txt +ejtraderRL/agent/__init__.py +ejtraderRL/agent/dqn.py +ejtraderRL/agent/qrdqn.py +ejtraderRL/data/__init__.py +ejtraderRL/data/get_data.py +ejtraderRL/nn/__init__.py +ejtraderRL/nn/build_model.py +ejtraderRL/nn/block/__init__.py +ejtraderRL/nn/block/conv.py +ejtraderRL/nn/block/convnext.py +ejtraderRL/nn/block/efficient.py +ejtraderRL/nn/layers/__init__.py +ejtraderRL/nn/layers/activation.py +ejtraderRL/nn/layers/attention.py +ejtraderRL/nn/layers/core.py +ejtraderRL/nn/layers/depthwiseconv1d.py +ejtraderRL/nn/layers/lambdalayer.py +ejtraderRL/nn/layers/pyconv.py +ejtraderRL/nn/layers/skconv.py +ejtraderRL/nn/layers/transformer.py +ejtraderRL/nn/losses/__init__.py +ejtraderRL/nn/losses/losses.py +ejtraderRL/nn/model/__init__.py +ejtraderRL/nn/model/ej.py \ No newline at end of file diff --git a/ejtraderRL.egg-info/dependency_links.txt b/ejtraderRL.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ejtraderRL.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/ejtraderRL.egg-info/requires.txt b/ejtraderRL.egg-info/requires.txt new file mode 100644 index 0000000..170bb21 --- /dev/null +++ b/ejtraderRL.egg-info/requires.txt @@ -0,0 +1,6 @@ +numpy +ta +pandas +pandas_datareader +matplotlib +IPython diff --git a/ejtraderRL.egg-info/top_level.txt b/ejtraderRL.egg-info/top_level.txt new file mode 100644 index 0000000..19b406e --- /dev/null +++ b/ejtraderRL.egg-info/top_level.txt @@ -0,0 +1 @@ +ejtraderRL diff --git a/ejtraderRL/__init__.py b/ejtraderRL/__init__.py new file mode 100644 index 0000000..401e892 --- /dev/null +++ b/ejtraderRL/__init__.py @@ -0,0 +1,4 @@ +from traderl import agent +from traderl import nn +from traderl import data + diff --git a/ejtraderRL/agent/__init__.py b/ejtraderRL/agent/__init__.py new file mode 100644 index 0000000..b9244e5 --- /dev/null +++ b/ejtraderRL/agent/__init__.py @@ -0,0 +1,2 @@ +from .dqn import * +from .qrdqn import * \ No newline at end of file diff --git a/ejtraderRL/agent/dqn.py b/ejtraderRL/agent/dqn.py new file mode 100644 index 0000000..99e376d --- /dev/null +++ b/ejtraderRL/agent/dqn.py @@ -0,0 +1,410 @@ +import warnings + +import matplotlib.pyplot as plt +import numpy as np +import tensorflow as tf +import pandas as pd +from IPython.display import clear_output +from ejtraderRL import nn +import ta + +warnings.simplefilter('ignore') + + +class DQN: + agent_name = "dqn" + loss = nn.losses.DQNLoss + + def __init__(self, df: pd.DataFrame, model_name, lr=1e-4, pip_scale=25, n=3, use_device="cpu", + gamma=0.99, train_spread=0.2, spread=10, balance=1000, risk=0.01): + """ + :param df: pandas dataframe or csv file. Must contain open, low, high, close + :param lr: learning rate + :param model_name: None or model name, If None -> model is not created. + :param pip_scale: Controls the degree of overfitting + :param n: int + :param use_device: tpu or gpu or cpu + :param gamma: float + :param train_spread: Determine the degree of long-term training. The smaller the value, the more short-term the trade. + :param spread: Cost of Trade + :param balance: Account size + :param risk: What percentage of the balance is at risk + """ + + self.df = df + self.model_name = model_name + self.lr = lr + self.pip_scale = pip_scale + self.n = n + self.use_device = use_device.lower() + self.gamma = gamma + self.train_spread = train_spread + self.spread = spread + self.risk = risk + + self.actions = {0: 1, 1: -1} + + if self.use_device == "tpu": + try: + resolver = tf.distribute.cluster_resolver.TPUClusterResolver(tpu='') + tf.config.experimental_connect_to_cluster(resolver) + tf.tpu.experimental.initialize_tpu_system(resolver) + self.strategy = tf.distribute.TPUStrategy(resolver) + except: + self.use_device = "cpu" + + self.train_rewards, self.test_rewards = [], [] + self.max_profits, self.max_pips = [], [] + self.train_loss, self.val_loss = [], [] + self.test_pip, self.test_profit = [], [] + + self.x, self.y, self.atr = self.env() + self.train_step = np.arange(int(len(self.y) * 0.9)) + self.test_step = np.arange(self.train_step[-1], len(self.y)) + + if self.model_name: + self.model, self.target_model = self.build_model() + + self.states, self.new_states, self.returns = self.train_data() + self.ind = np.arange(len(self.returns)) + np.random.shuffle(self.ind) + + self.max_profit, self.max_pip = 0, 0 + self.now_max_profit, self.now_max_pip = 0, 0 + + self.account_size = balance + + def env(self): + if isinstance(self.df, str): + self.df = pd.read_csv(self.df) + + self.df.columns = self.df.columns.str.lower() + + self.df["sig"] = self.df.close - self.df.close.shift(1) + self.df["atr"] = ta.volatility.average_true_range(self.df.high, self.df.low, self.df.close) + self.df = self.df.dropna() + + x = [] + y = [] + atr = [] + + window_size = 30 + for i in range(window_size, len(self.df.close)): + x.append(self.df.sig[i - window_size:i]) + y.append(self.df.close[i - 1]) + atr.append(self.df.atr[i - 1]) + + x = np.array(x, np.float32).reshape((-1, 30, 1)) + y = np.array(y, np.int32).reshape((-1,)) + atr = np.array(atr, np.int32).reshape((-1,)) + + return x, y, atr + + def _build_model(self) -> nn.model.Model: + model = nn.build_model(self.model_name, self.x.shape[1:], 2, None, self.agent_name) + model.compile( + tf.keras.optimizers.Adam(self.lr, clipnorm=1.), loss=self.loss(), steps_per_execution=100 + ) + return model + + def build_model(self): + if self.use_device == "tpu": + with self.strategy.scope(): + model = self._build_model() + target_model = tf.keras.models.clone_model(model) + else: + model = self._build_model() + target_model = tf.keras.models.clone_model(model) + target_model.set_weights(model.get_weights()) + + return model, target_model + + def train_data(self): + h, h_ = 0, self.train_step[-1] + n = self.n + + states = self.x[h:h_ - n].copy() + close = self.y[h:h_] + + buy = np.array([close[i + n] - close[i] for i in range(len(close) - n)]).reshape((-1,)) + scale = np.quantile(abs(buy), 0.99) + buy = np.clip(buy / scale, -1, 1) * self.pip_scale + sell = -buy + + spread = self.train_spread * self.pip_scale + + returns = np.zeros((len(close) - n, 2, 2)) + returns[:, 0, 0] = buy + returns[:, 0, 1] = sell - spread + returns[:, 1, 0] = buy - spread + returns[:, 1, 1] = sell + + new_states = np.roll(states, -n, axis=0)[:-n] + states = states[:-n] + returns = returns[:-n] + + return states, new_states, returns + + def get_actions(self, df): + q = self.model.predict(df, 102800, workers=10000, use_multiprocessing=True) + actions = np.argmax(q, -1) + a = np.argmax([q[0, 0, 1], q[0, 1, 0]]) + act = [a] + + for i in range(1, len(actions)): + a = actions[i, a] + act.append(a) + + return np.array(act).reshape((-1,)) + + def trade(self, h, h_): + df = self.x[h:h_] + trend = self.y[h:h_] + atr = self.atr[h:h_] + profit = self.account_size + + actions = self.get_actions(df) + + old_a = actions[0] + old_price = trend[0] + + total_pip, total_profit = 0, 0 + self.now_max_profit, self.now_max_pip = 0, 0 + pips, profits = [], [] + total_pips, total_profits = [], [] + buy, sell = [], [] + + if old_a == 0: + buy.append(0) + else: + sell.append(0) + + loss_cut = -atr[0] * 2 + position_size = int((profit * self.risk) / -loss_cut) + position_size = np.minimum(position_size, 500 * 200 * 100) + position_size = np.maximum(position_size, 1) + + for i, (act, price, atr) in enumerate(zip(actions, trend, atr)): + if old_a != act: + old_a = self.actions[old_a] + pip = (price - old_price) * old_a + total_pip += pip - self.spread + pips.append(pip - self.spread) + total_pips.append(total_pip) + + gain = pip * position_size - self.spread * position_size + total_profit += gain + profits.append(gain) + total_profits.append(total_profit) + + self.now_max_pip = np.maximum(self.now_max_pip, total_pip) + self.now_max_profit = np.maximum(self.now_max_profit, total_profit) + + old_price = price + old_a = act + + loss_cut = -atr * 2 + position_size = int((profit * self.risk) / -loss_cut) + position_size = np.minimum(position_size, 500 * 200 * 100) + position_size = np.maximum(position_size, 0) + + if act == 0: + buy.append(i) + else: + sell.append(i) + + pips = np.array(pips) + + return pips, profits, total_pips, total_profits, total_pip, total_profit, buy, sell + + def evolute(self, h, h_): + pips, profits, total_pips, total_profits, total_pip, total_profit, buy, sell = self.trade(h, h_) + + acc = np.mean(pips > 0) + total_win = np.sum(pips[pips > 0]) + total_lose = np.sum(pips[pips < 0]) + rr = total_win / abs(total_lose) + ev = (np.mean(pips[pips > 0]) * acc + np.mean(pips[pips < 0]) * (1 - acc)) / abs(np.mean(pips[pips < 0])) + + plt.figure(figsize=(10, 5), dpi=100) + plt.subplot(1, 2, 1) + plt.plot(total_pips) + plt.subplot(1, 2, 2) + plt.plot(total_profits) + plt.show() + + print( + f"acc = {acc}, pips = {sum(pips)}\n" + f"total_win = {total_win}, total_lose = {total_lose}\n" + f"rr = {rr}, ev = {ev}\n" + ) + + def plot_trade(self, train=False, test=False, period=1): + assert train or test + h = 0 + if test: + h = self.test_step[0] + elif train: + h = np.random.randint(0, int(self.train_step[-1] - 960 * period)) + h_ = h + len(self.train_step) // 12 * period + trend = self.y[h:h_] + + pips, profits, total_pips, total_profits, total_pip, total_profit, buy, sell = self.trade(h, h_) + + plt.figure(figsize=(20, 10), dpi=100) + plt.plot(trend, color="g", alpha=1, label="close") + plt.plot(trend, "^", markevery=buy, c="red", label='buy', alpha=0.7) + plt.plot(trend, "v", markevery=sell, c="blue", label='sell', alpha=0.7) + + plt.legend() + plt.show() + + print(f"pip = {np.sum(pips)}" + f"\naccount size = {total_profit}" + f"\ngrowth rate = {total_profit / self.account_size}" + f"\naccuracy = {np.mean(np.array(pips) > 0)}") + + def plot_result(self, w, risk=0.1): + self.model.set_weights(w) + self.risk = risk + + plt.figure(figsize=(20, 5), dpi=100) + plt.subplot(1, 2, 1) + plt.plot(self.test_pip) + plt.subplot(1, 2, 2) + plt.plot(self.test_profit) + plt.show() + + ################################################################################ + self.plot_trade(train=False, test=True, period=9) + ################################################################################ + plt.figure(figsize=(10, 5)) + plt.plot(self.train_loss) + plt.plot(self.val_loss) + plt.title('Model loss') + plt.ylabel('Loss') + plt.xlabel('Epoch') + plt.legend(['Train', 'Validation'], loc='upper left') + plt.show() + + len_ = len(self.train_loss) // 2 + plt.figure(figsize=(10, 5)) + plt.plot(self.train_loss[len_:]) + plt.plot(self.val_loss[len_:]) + plt.title('Model loss') + plt.ylabel('Loss') + plt.xlabel('Epoch') + plt.legend(['Train', 'Validation'], loc='upper left') + plt.show() + ################################################################################ + self.evolute(self.test_step[0], self.test_step[-1]) + ################################################################################ + plt.figure(figsize=(20, 5), dpi=100) + plt.subplot(1, 2, 1) + plt.plot(self.train_rewards) + plt.subplot(1, 2, 2) + plt.plot(self.test_rewards) + plt.show() + + print(f"profits = {self.max_profit}, max profits = {self.max_profits}\n" + f"pips = {self.max_pip}, max pip = {self.max_pips}") + ################################################################################ + self.evolute(self.test_step[0] - len(self.train_step), self.test_step[0]) + + def target_q(self, returns, target_q, target_a): + if self.train_loss: + target_a = np.argmax(target_a, -1) + rr = range(len(returns)) + returns[:, 0, 0] += self.gamma * target_q[rr, 0, target_a[rr, 0]] + returns[:, 0, 1] += self.gamma * target_q[rr, 1, target_a[rr, 1]] + returns[:, 1, 0] += self.gamma * target_q[rr, 0, target_a[rr, 0]] + returns[:, 1, 1] += self.gamma * target_q[rr, 1, target_a[rr, 1]] + + assert np.mean(np.isnan(returns) == False) == 1 + + return returns + + def _train(self, epoch, batch_size): + ind = self.ind + + states, new_states, returns = self.states[ind].copy(), self.new_states[ind].copy(), self.returns[ind].copy() + + if self.train_loss: + target_q = self.target_model.predict(new_states, 102800) + else: + target_q = np.zeros((len(returns), 2, 2), np.float32) + + for _ in range(epoch): + returns = self.returns[ind].copy() + noise = np.random.normal(0, 0.1, states.shape) + + target_a = self.model.predict(new_states + noise, 102800) + returns = self.target_q(returns, target_q, target_a) + + h = self.model.fit(states + noise, returns, batch_size, validation_split=0.2) + self.train_loss.extend(h.history["loss"]) + self.val_loss.extend(h.history["val_loss"]) + + p = 12 + + if len(self.train_loss) >= 200: + + pips, profits, total_pips, total_profits, total_pip, total_profit, buy, sell = \ + self.trade(self.test_step[0] - len(self.test_step), self.test_step[0]) + self.train_rewards.append(np.sum(pips)) + pips, profits, total_pips, total_profits, total_pip, total_profit, buy, sell = \ + self.trade(self.test_step[0], self.test_step[-1]) + self.test_rewards.append(np.sum(pips)) + + acc = np.mean(pips > 0) + + total_win = np.sum(pips[pips > 0]) + total_lose = np.sum(pips[pips < 0]) + ev = \ + (np.mean(pips[pips > 0]) * acc + np.mean(pips[pips < 0]) * (1 - acc)) / abs(np.mean(pips[pips < 0])) + ev = np.clip(ev, 0, 0.75) / 0.75 + rr = np.clip(total_win / abs(total_lose), 0, 2.5) / 2.5 + acc /= 0.7 + + self.max_pip = (rr + ev + acc) * np.clip(np.sum(profits) / self.account_size, 1, None) + self.max_pip = 0 if np.isnan(self.max_pip) else self.max_pip + + self.max_profit /= self.account_size + + self.test_pip.append(self.max_pip) + self.test_profit.append(self.max_profit) + + if len(pips) >= (p * 5): + if self.max_pips <= self.max_pip: + self.best_w = self.model.get_weights() + self.max_profits = self.max_profit + + self.max_profits = np.maximum(self.max_profit, self.max_profits) + self.max_pips = np.maximum(self.max_pip, self.max_pips) + + plt.figure(figsize=(20, 5), dpi=100) + plt.subplot(1, 2, 1) + plt.plot(self.train_rewards) + plt.subplot(1, 2, 2) + plt.plot(self.test_rewards) + plt.show() + + print(f"profits = {self.max_profit}, max profits = {self.max_profits}\n" + f"pips = {self.max_pip}, max pip = {self.max_pips}") + + def train(self, epoch=40, batch_size=2056): + for _ in range(600 // epoch): + clear_output() + plt.figure(figsize=(10, 5)) + plt.plot(self.train_loss) + plt.plot(self.val_loss) + plt.title('Model loss') + plt.ylabel('Loss') + plt.xlabel('Epoch') + plt.legend(['Train', 'Validation'], loc='upper left') + plt.show() + self._train(epoch, batch_size) + self.target_model.set_weights(self.model.get_weights()) + + +__all__ = ["DQN"] diff --git a/ejtraderRL/agent/qrdqn.py b/ejtraderRL/agent/qrdqn.py new file mode 100644 index 0000000..b73481d --- /dev/null +++ b/ejtraderRL/agent/qrdqn.py @@ -0,0 +1,48 @@ +import warnings + +import numpy as np + +from ejtraderRL import nn +from ejtraderRL.agent import DQN + +warnings.simplefilter('ignore') + + +class QRDQN(DQN): + agent_name = "QRDQN" + agent_loss = nn.losses.QRDQNLoss + + def get_actions(self, df): + q = self.model.predict(df, 10280, workers=10000, use_multiprocessing=True) + q = np.mean(q, 2) + actions = np.argmax(q, -1) + a = np.argmax([q[0, 0, 1], q[0, 1, 0]]) + act = [a] + + for i in range(1, len(actions)): + a = actions[i, a] + act.append(a) + + return act + + def target_q(self, returns, target_q, target_a): + returns = np.reshape(returns, (-1, 2, 1, 2)) + returns = np.tile(returns, (1, 1, 32, 1)) + + if self.train_loss and target_q.shape == returns.shape: + target_a = np.argmax(np.mean(target_a, axis=2), -1) + rr = range(len(returns)) + returns[:, 0, :, 0] += self.gamma * target_q[rr, 0, :, target_a[:, 0]] + returns[:, 0, :, 1] += self.gamma * target_q[rr, 1, :, target_a[:, 1]] + returns[:, 1, :, 0] += self.gamma * target_q[rr, 0, :, target_a[:, 0]] + returns[:, 1, :, 1] += self.gamma * target_q[rr, 1, :, target_a[:, 1]] + + assert np.mean(np.isnan(returns) == False) == 1 + + return returns + + def train(self, epoch=50, batch_size=2056): + super(QRDQN, self).train(epoch, batch_size) + + +__all__ = ["QRDQN"] \ No newline at end of file diff --git a/ejtraderRL/data/__init__.py b/ejtraderRL/data/__init__.py new file mode 100644 index 0000000..01acc7c --- /dev/null +++ b/ejtraderRL/data/__init__.py @@ -0,0 +1 @@ +from .get_data import * \ No newline at end of file diff --git a/ejtraderRL/data/get_data.py b/ejtraderRL/data/get_data.py new file mode 100644 index 0000000..a7141f4 --- /dev/null +++ b/ejtraderRL/data/get_data.py @@ -0,0 +1,23 @@ +import pandas as pd +import pandas_datareader as pdr + + +def get_forex_data(symbol: str, timeframe: str): + """ + :param symbol: AUDJPY, AUDUSD, EURCHF, EURGBP, EURJPY, EURUSD, GBPJPY, GBPUSD, USDCAD, USDCHF, USDJPY, XAUUSD + :param timeframe: m15, m30, h1, h4, d1 + :return: pandas DataFrame + """ + symbol = symbol.upper() + + url = "https://raw.githubusercontent.com/komo135/forex-historical-data/main/" + url += symbol + "/" + symbol + timeframe.lower() + ".csv" + + return pd.read_csv(url) + + +def get_stock_data(symbol, start=None, end=None): + return pdr.get_data_yahoo(symbol, strt=start, end=end) + + +__all__ = ["get_stock_data", "get_forex_data"] \ No newline at end of file diff --git a/ejtraderRL/nn/__init__.py b/ejtraderRL/nn/__init__.py new file mode 100644 index 0000000..509ca3b --- /dev/null +++ b/ejtraderRL/nn/__init__.py @@ -0,0 +1,5 @@ +from . import layers +from . import model +from . import block +from . import losses +from .build_model import * diff --git a/ejtraderRL/nn/block/__init__.py b/ejtraderRL/nn/block/__init__.py new file mode 100644 index 0000000..cd3ba4d --- /dev/null +++ b/ejtraderRL/nn/block/__init__.py @@ -0,0 +1,3 @@ +from .conv import * +from .convnext import * +from .efficient import * diff --git a/ejtraderRL/nn/block/conv.py b/ejtraderRL/nn/block/conv.py new file mode 100644 index 0000000..eeba260 --- /dev/null +++ b/ejtraderRL/nn/block/conv.py @@ -0,0 +1,78 @@ +import tensorflow as tf +from tensorflow.keras import layers +from traderl.nn.layers import layer, Activation +import numpy as np + + +class ConvBlock(layers.Layer): + def __init__(self, dim, layer_name="Conv1D", types="resnet", + groups=1, bias=True, attention=None, noise=layers.Dropout, noise_r=0, **kwargs): + """ + :param dim: output dimention + :param layer_name: layer name + :param types: "densenet" or "resnet" + """ + super(ConvBlock, self).__init__() + + self.dim = dim + self.layer_name = layer_name + self.types = types.lower() + self.groups = groups + self.bias = bias + self.attention = attention + self.noise = noise + self.noise_r = noise_r + + assert self.types == "densenet" or self.types == "resnet" + + if self.types == "resnet": + self.l = [ + Activation(), + noise(noise_r), + layer(layer_name, dim, bias, 7, groups, **kwargs), + Activation(), + layer("conv1d", dim * 4, True, 1, 1, **kwargs), + Activation(), + layer(attention, dim * 4, **kwargs), + noise(noise_r), + layer("conv1d", dim, True, 1, 1, **kwargs), + ] + else: + self.l = [ + Activation(), + noise(noise_r), + layer("conv1d", dim * 4, True, 1, 1, **kwargs), + Activation(), + layer(attention, dim * 4, **kwargs), + noise(noise_r), + layer(layer_name, dim, bias, 7, groups), + ] + + self.l = np.array(self.l) + self.l = list(self.l[self.l != None].reshape((-1,))) + + def call(self, inputs, *args, **kwargs): + x = inputs + for l in self.l: + x = l(x) + + if self.types == "densenet": + return tf.concat([inputs, x], axis=-1) + elif self.types == "resnet": + return tf.add(inputs, x) + + def get_config(self): + config = { + "dim": self.dim, + "layer_name": self.layer_name, + "types": self.types, + "groups": self.groups, + "bias": self.bias, + "attention": self.attention, + "noise": self.noise, + "noise_r": self.noise_r + } + return config + + +__all__ = ["ConvBlock"] diff --git a/ejtraderRL/nn/block/convnext.py b/ejtraderRL/nn/block/convnext.py new file mode 100644 index 0000000..d8de673 --- /dev/null +++ b/ejtraderRL/nn/block/convnext.py @@ -0,0 +1,52 @@ +import numpy as np +import tensorflow as tf +from tensorflow.keras import layers + +from traderl.nn.layers import layer + + +class ConvnextBlock(layers.Layer): + def __init__(self, dim: int, layer_name: str, types: str, attention=None, noise=layers.Dropout, noise_r=0, + **kwargs): + super(ConvnextBlock, self).__init__() + self.dim = dim + self.layer_name = layer_name + self.types = types + self.attention = attention + + self.l = [ + noise(noise_r), + layer(layer_name, dim, True, 7), + layers.LayerNormalization(), + layer("conv1d", dim * 4, True, 1, 1, **kwargs), + layers.Activation("gelu"), + layer(attention, dim * 4), + noise(noise_r), + layer("conv1d", dim, True, 1, 1, **kwargs), + ] + self.l = np.array(self.l) + self.l = list(self.l[self.l != None].reshape((-1,))) + + def call(self, inputs, *args, **kwargs): + x = inputs + for l in self.l: + x = l(x) + + if self.types == "resnet": + return inputs + x + elif self.types == "densenet": + return tf.concat([inputs, x], -1) + + def get_config(self): + config = { + "dim": self.dim, + "layer_name": self.layer_name, + "types": self.types, + "attention": self.attention, + "noise": self.noise, + "noise_r": self.noise_r + } + return config + + +__all__ = ["ConvnextBlock"] diff --git a/ejtraderRL/nn/block/efficient.py b/ejtraderRL/nn/block/efficient.py new file mode 100644 index 0000000..15675f4 --- /dev/null +++ b/ejtraderRL/nn/block/efficient.py @@ -0,0 +1,109 @@ +import numpy as np +import tensorflow as tf +from tensorflow.keras import layers + +from traderl.nn.layers import SE, Activation, layer + + +class MBBlock(layers.Layer): + def __init__(self, idim, odim, expand_ratio, kernel_size, + se_ratio=0.25, layer_name="DepthwiseConv1D", types="resnet", noise=layers.Dropout, noise_r=0, **kwargs): + super(MBBlock, self).__init__() + self.idim = idim + self.odim = odim + self.expand_ratio = expand_ratio + self.se_ratio = se_ratio + self.kernel_size = kernel_size + self.layer_name = layer_name.lower() + self.types = types.lower() + self.noise = noise + self.noise_r = noise_r + assert self.types == "resnet" or self.types == "densenet" + + self.l = np.array([ + Activation(), + noise(noise_r), + layer("conv1d", int(idim * expand_ratio), False, 1, 1), + Activation(), + layer(layer_name, int(idim * expand_ratio), False, kernel_size, 1), + Activation(), + SE(int(idim * expand_ratio), se_ratio) if se_ratio > 0 else None, + noise(noise_r), + layer("conv1d", odim, False, 1, 1) + ]) + self.l = list(self.l[self.l is not None].reshape((-1,))) + + def call(self, inputs, *args, **kwargs): + x = inputs + for l in self.l: + x = l(x) + + if self.types == "resnet": + return x + inputs + elif self.types == "densenet": + return tf.concat([inputs, x], axis=-1) + + def get_config(self): + config = { + "idim": self.idim, + "odim": self.odim, + "expand_ratio": self.expand_ratio, + "se_ratio": self.se_ratio, + "kernel_size": self.kernel_size, + "layer_name": self.layer_name, + "types": self.types, + "noise": self.noise, + "noise_r": self.noise_r + } + return config + + +class FuseBlock(layers.Layer): + def __init__(self, idim, odim, expand_ratio, kernel_size, + se_ratio=0.25, layer_name="DepthwiseConv1D", types="resnet", noise=layers.Dropout, noise_r=0, **kwargs): + super(FuseBlock, self).__init__() + self.idim = idim + self.odim = odim + self.expand_ratio = expand_ratio + self.se_ratio = se_ratio + self.kernel_size = kernel_size + self.layer_name = layer_name.lower() + self.types = types.lower() + + self.l = np.array([ + Activation("mish"), + noise(noise_r), + layer("conv1d", int(idim * expand_ratio), False, 1, 1), + SE(int(idim * expand_ratio), se_ratio) if se_ratio > 0 else None, + Activation("mish"), + noise(noise_r), + layer("conv1d", odim, False, 1, 1), + ]) + self.l = list(self.l[self.l != None].reshape((-1,))) + + def call(self, inputs, *args, **kwargs): + x = inputs + for l in self.l: + x = l(x) + + if self.types == "resnet": + return x + inputs + elif self.types == "densenet": + return tf.concat([inputs, x], axis=-1) + + def get_config(self): + config = { + "idim": self.idim, + "odim": self.odim, + "expand_ratio": self.expand_ratio, + "se_ratio": self.se_ratio, + "kernel_size": self.kernel_size, + "layer_name": self.layer_name, + "types": self.types, + "noise": self.noise, + "noise_r": self.noise_r + } + return config + + +__all__ = ["MBBlock", "FuseBlock"] \ No newline at end of file diff --git a/ejtraderRL/nn/build_model.py b/ejtraderRL/nn/build_model.py new file mode 100644 index 0000000..5da32f6 --- /dev/null +++ b/ejtraderRL/nn/build_model.py @@ -0,0 +1,207 @@ +import numpy as np +import tensorflow as tf +from tensorflow.keras.layers import GaussianDropout, Conv1D, LayerNormalization + +from .block import ConvBlock, ConvnextBlock, FuseBlock, MBBlock +from .layers import Activation, inputs_f, Output, DQNOutput, QRDQNOutput +from .model import * + +output_dict = {None: Output, "dqn": DQNOutput, "qrdqn": QRDQNOutput} + +gamma = [1, 1.2, 1.4, 1.7, 2., 2.4, 2.9, 3.5] +alpha = [1, 1.1, 1.21, 1.33, 1.46, 1.61, 1.77, 1.94] + +l_b0 = (3, 4, 6, 3) + +noise = GaussianDropout +noise_b0 = [0.1, 0.1, 0.1] +noise_b1 = [0.1, 0.1, 0.1] +noise_b2 = [0.1, 0.1, 0.1] +noise_b3 = [0.1, 0.1, 0.1] +noise_b4 = [0.1, 0.1, 0.1] +noise_b5 = [0.1, 0.1, 0.1] +noise_b6 = [0.1, 0.1, 0.1] +noise_b7 = [0.1, 0.1, 0.1] +noise_l = [noise_b0, noise_b1, noise_b2, noise_b3, noise_b4, noise_b5, noise_b6, noise_b7] + + +class BuildModel: + def __init__(self, num_layer, dim: int, layer_name: str, types: str, scale=0, + groups=1, ej=False, se=False, cbam=False, vit=False, + efficientv1=False, efficientv2=False, + convnext=False): + self.num_layer = num_layer + self.layer_name = layer_name.lower() + self.types = types + self.groups = groups + self.ej = bool(ej) + self.se = bool(se) + self.cbam = bool(cbam) + self.efficientv1 = bool(efficientv1) + self.efficientv2 = bool(efficientv2) + self.vit = vit + self.convnext = convnext + self.attn = "se" if se else "cbam" if cbam else None + + self.noise_ratio = noise_l[scale] + self.gamma = gamma[scale] + self.alpha = alpha[scale] + self.dim = int(dim * self.gamma) if dim else dim + self.num_layer = num_layer if num_layer else np.round(np.array(l_b0) * self.alpha).astype(int) + + if efficientv1 or efficientv2: + self.dim = int((16 if self.types == "resnet" else 32) * self.gamma) + if self.dim and self.layer_name == "lambdalayer": + self.dim = 4 * int(np.round(self.dim / 4)) + + def build_eff_block(self, l): + block = None + if self.efficientv1: + block = [MBBlock for _ in range(len(l))] + elif self.efficientv2: + block = [FuseBlock, FuseBlock, FuseBlock] + block.extend([MBBlock for _ in range(len(l) - 3)]) + + self.block = block + + def transition(self, x, dim=None, pool=True): + if self.types == "densenet": + dim = x.shape[-1] // 2 if pool else x.shape[-1] + elif self.types == "resnet": + dim = self.dim = self.dim * 2 if dim is None else dim + + if self.convnext: + x = LayerNormalization()(x) + x = Conv1D(dim, 2, 1, "same", kernel_initializer="he_normal")(x) + if pool: + x = tf.keras.layers.AvgPool1D()(x) + x = LayerNormalization()(x) + else: + x = Activation()(x) + x = Conv1D(dim, 1, 1, "same", kernel_initializer="he_normal")(x) + if pool: + x = tf.keras.layers.AvgPool1D()(x) + + return x + + def efficient_model(self, x): + l = [1, 2, 2, 3, 3, 4, 1] + k = [3, 3, 5, 3, 5, 5, 3] + pool = [False, False, True, True, True, False, True] + self.build_eff_block(l) + + if self.types == "resnet": + ic = [16, 16, 24, 40, 80, 112, 192] + oc = [16, 24, 40, 80, 112, 192, 320] + ep = [1, 6, 6, 6, 6, 6, 6] + else: + ic = [32 for _ in range(len(l))] + oc = ic + ep = [6 for _ in range(len(l))] + + ic = (np.array(ic) * self.gamma).astype(np.int32) + oc = (np.array(oc) * self.gamma).astype(np.int32) + l = np.round(np.array(l) * self.alpha).astype(np.int32) + + if self.layer_name == "lambdalayer": + ic = [int(4 * np.round(ic / 4)) for ic in ic] + oc = [int(4 * np.round(oc / 4)) for oc in oc] + + for e, (ic, oc, ep, l, k, pool, block) in enumerate(zip(ic, oc, ep, l, k, pool, self.block)): + + if e != 0: + x = self.transition(x, oc, pool) + + for _ in range(l): + x = block(ic, oc, ep, k, 0.25, self.layer_name, self.types, noise, self.noise_ratio[1])(x) + + return x + + def conv_model(self, x): + + for i, l in enumerate(self.num_layer): + if i != 0: + x = self.transition(x, None, True) + + for _ in range(l): + if self.convnext: + x = ConvnextBlock(self.dim, self.layer_name, self.types, self.attn, noise, self.noise_ratio[1])(x) + else: + x = ConvBlock(self.dim, self.layer_name, self.types, self.groups, True, self.attn, noise, + self.noise_ratio[1])(x) + + return x + + def build_model(self, input_shape, output_size, output_activation, agent=None): + inputs, x = inputs_f(input_shape, self.dim, 5, 1, False, "same", noise, self.noise_ratio[0]) + + if self.efficientv1 or self.efficientv2: + x = self.efficient_model(x) + else: + x = self.conv_model(x) + + x = tf.keras.layers.GlobalAvgPool1D()(x) + + x = output_dict[agent](output_size, output_activation, noise, self.noise_ratio[2])(x) + + return EJModel(inputs, x) if self.ej else Model(inputs, x) + + +network_dict = {} +available_network = [] + + +def create_network(name, num_layer, dim, layer_name, types, **kwargs): + available_network.append(f"{name}") + for i in range(8): + network_dict.update({f"{name}_b{i}": lambda i=i: BuildModel(num_layer, dim, layer_name, types, i, **kwargs)}) + + +create_network("efficientnet", [], 0, "DepthwiseConv1D", "resnet", efficientv1=True) +create_network("ej_efficientnet", [], 0, "DepthwiseConv1D", "resnet", efficientv1=True, ej=True) + +create_network("dense_efficientnet", [], 0, "DepthwiseConv1D", "densenet", efficientv1=True) +create_network("ej_dense_efficientnet", [], 0, "DepthwiseConv1D", "densenet", efficientv1=True, ej=True) + +create_network("lambda_efficientnet", [], 0, "lambdalayer", "resnet", efficientv1=True) +create_network("ej_lambda_efficientnet", [], 0, "lambdalayer", "resnet", efficientv1=True, ej=True) + +create_network("efficientnetv2", [], 0, "DepthwiseConv1D", "resnet", efficientv2=True) +create_network("ej_efficientnetv2", [], 0, "DepthwiseConv1D", "resnet", efficientv2=True, ej=True) + +create_network("resnet", [], 48, "Conv1D", "resnet") +create_network("ej_resnet", [], 48, "Conv1D", "resnet", ej=True) +create_network("se_resnet", [], 48, "Conv1D", "resnet", se=True) +create_network("ej_se_resnet", [], 48, "Conv1D", "resnet", se=True, ej=True) + +create_network("densenet", [], 48, "Conv1D", "densenet") +create_network("ej_densenet", [], 48, "Conv1D", "densenet", ej=True) +create_network("se_densenet", [], 48, "Conv1D", "densenet", se=True) +create_network("ej_se_densenet", [], 48, "Conv1D", "densenet", se=True, ej=True) + +create_network("lambda_resnet", [], 48, "LambdaLayer", "resnet") +create_network("ej_lambda_resnet", [], 48, "LambdaLayer", "resnet", ej=True) +create_network("se_lambda_resnet", [], 48, "LambdaLayer", "resnet", se=True) +create_network("ej_se_lambda_resnet", [], 48, "LambdaLayer", "resnet", se=True, ej=True) + +create_network("convnext", [], 48, "DepthwiseConv1D", "resnet", convnext=True) +create_network("ej_convnext", [], 48, "DepthwiseConv1D", "resnet", convnext=True, ej=True) +create_network("se_convnext", [], 48, "DepthwiseConv1D", "resnet", convnext=True, se=True) +create_network("ej_se_convnext", [], 48, "DepthwiseConv1D", "resnet", convnext=True, se=True, ej=True) + +create_network("lambda_convnext", [], 48, "LambdaLayer", "resnet", convnext=True) +create_network("ej_lambda_convnext", [], 48, "LambdaLayer", "resnet", convnext=True, ej=True) +create_network("se_lambda_convnext", [], 48, "LambdaLayer", "resnet", convnext=True, se=True) +create_network("ej_se_lambda_convnext", [], 48, "LambdaLayer", "resnet", convnext=True, se=True, ej=True) + + +def build_model(model_name: str, input_shape: tuple, output_size: int, output_activation=None, agent=None) -> Model: + model = network_dict[model_name]() + model = model.build_model(input_shape, output_size, output_activation, agent) + + return model + + +available_network = np.array(available_network).reshape((-1,)) + +__all__ = ["build_model", "available_network", "network_dict", "BuildModel"] diff --git a/ejtraderRL/nn/layers/__init__.py b/ejtraderRL/nn/layers/__init__.py new file mode 100644 index 0000000..e8da03d --- /dev/null +++ b/ejtraderRL/nn/layers/__init__.py @@ -0,0 +1,26 @@ +from .core import * +from .activation import * +from .attention import * +from .transformer import * +from .pyconv import * +from .skconv import * +from .lambdalayer import * +from .depthwiseconv1d import * +from tensorflow.keras.layers import Conv1D + + +def layer(layer_name: str, dim, use_bias=True, kernel_size=1, groups=1, **kwargs): + if layer_name: + layer_name = layer_name.lower() + if layer_name == "conv1d": + return Conv1D(dim, kernel_size, 1, "same", kernel_initializer="he_normal", use_bias=use_bias, groups=groups, **kwargs) + elif layer_name == "lambdalayer": + return LambdaLayer(dim, **kwargs) + elif layer_name == "skconv": + return SKConv(dim, groups, **kwargs) + elif layer_name == "depthwiseconv1d": + return DepthwiseConv1D(kernel_size, 1, "same", kernel_initializer="he_normal", use_bias=use_bias) + elif layer_name == "se": + return SE(dim, **kwargs) + elif layer_name == "cbam": + return CBAM(dim, **kwargs) diff --git a/ejtraderRL/nn/layers/activation.py b/ejtraderRL/nn/layers/activation.py new file mode 100644 index 0000000..a1b5873 --- /dev/null +++ b/ejtraderRL/nn/layers/activation.py @@ -0,0 +1,35 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf + + +class Mish(layers.Layer): + def call(self, inputs, *args, **kwargs): + return inputs * tf.tanh(tf.math.softplus(inputs)) + + +class Activation(layers.Layer): + def __init__(self, activation="swish", normalization=layers.LayerNormalization): + super(Activation, self).__init__() + self.activation = activation.lower() + self.normalization = normalization + if self.activation == "mish": + self.act = Mish() + else: + self.act = layers.Activation(activation) + if normalization: + self.norm = normalization() + + def call(self, inputs, training=None, mask=None): + if self.normalization: + x = self.norm(inputs) + else: + x = inputs + return self.act(x) + + def get_config(self): + return {"activation": self.activation, "normalization": self.normalization} + + +__all__ = [ + "Activation" +] \ No newline at end of file diff --git a/ejtraderRL/nn/layers/attention.py b/ejtraderRL/nn/layers/attention.py new file mode 100644 index 0000000..b9cdfc9 --- /dev/null +++ b/ejtraderRL/nn/layers/attention.py @@ -0,0 +1,70 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf + + +class SE(layers.Layer): + def __init__(self, dim, r=.0625): + super(SE, self).__init__() + self.dim = dim + self.r = r + + self.mlp = Sequential([ + layers.Dense(int(dim * r), "relu", kernel_initializer="he_normal"), + layers.Dense(dim, "sigmoid") + ]) + + def call(self, inputs, *args, **kwargs): + x = tf.reduce_mean(inputs, axis=1, keepdims=True) + x = self.mlp(x) + x *= inputs + + return x + + def get_config(self): + config = { + "dim": self.dim, + "r": self.r + } + + return config + + +class CBAM(layers.Layer): + def __init__(self, filters, r=.0625): + super(CBAM, self).__init__() + self.filters = filters + self.r = r + + self.avg_pool = layers.GlobalAvgPool1D() + self.max_pool = layers.GlobalMaxPool1D() + self.mlp = [ + layers.Dense(int(filters * r), "relu", kernel_initializer="he_normal"), + layers.Dense(filters) + ] + + self.concat = layers.Concatenate() + self.conv = layers.Conv1D(1, 7, 1, "same", activation="sigmoid") + + def compute_mlp(self, x, pool): + x = pool(x) + for mlp in self.mlp: + x = mlp(x) + + return x + + def call(self, inputs, training=None, *args, **kwargs): + x = self.compute_mlp(inputs, self.avg_pool) + self.compute_mlp(inputs, self.max_pool) + x = inputs * tf.reshape(tf.nn.sigmoid(x), (-1, 1, self.filters)) + + conv = self.concat([tf.reduce_mean(x, -1, keepdims=True), tf.reduce_max(x, -1, keepdims=True)]) + return x * self.conv(conv) + + def get_config(self): + config = {"filters": self.filters, "r": self.r} + return config + + +__all__ = [ + "SE", + "CBAM" +] \ No newline at end of file diff --git a/ejtraderRL/nn/layers/core.py b/ejtraderRL/nn/layers/core.py new file mode 100644 index 0000000..8da8661 --- /dev/null +++ b/ejtraderRL/nn/layers/core.py @@ -0,0 +1,74 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf + + +def inputs_f(input_shape, dim, kernel_size, strides, pooling, padding="same", noise=layers.Dropout, noise_r=0): + inputs = tf.keras.layers.Input(input_shape) + x = noise(noise_r)(inputs) + x = layers.Conv1D(dim, kernel_size, strides, padding, kernel_initializer="he_normal")(x) + if pooling: + x = tf.keras.layers.AvgPool1D(3, 2)(x) + + return inputs, x + + +class Output(layers.Layer): + def __init__(self, output_size, activation=None, noise=layers.Dropout, noise_r=0): + super(Output, self).__init__() + self.output_size = output_size + self.activation = activation + self.noise = noise + self.noise_r = noise_r + self.out = Sequential([ + noise(noise_r), + layers.Dense(output_size, activation) + ]) + + def call(self, inputs, *args, **kwargs): + return self.out(inputs) + + def get_config(self): + return {"output_size": self.output_size, + "activation": self.activation, + "noise": self.noise, + "noise_r":self.noise_r} + + +class DQNOutput(Output): + def __init__(self, output_size, activation=None, noise=layers.Dropout, noise_r=0): + super(DQNOutput, self).__init__(output_size, activation, noise, noise_r) + + self.out = [ + [noise(noise_r), layers.Dense(output_size), layers.Reshape((output_size, 1))] + for _ in range(output_size) + ] + + def call(self, inputs, *args, **kwargs): + out = [] + for l1 in self.out: + q = inputs + for l2 in l1: + q = l2(q) + out.append(q) + + return tf.concat(out, axis=-1) + + +class QRDQNOutput(DQNOutput): + def __init__(self, output_size, activation=None, noise=layers.Dropout, noise_r=0, quantile_size=32): + super(QRDQNOutput, self).__init__(output_size, activation, noise, noise_r) + self.quantile_size = quantile_size + + self.out = [ + [noise(noise_r), layers.Dense(output_size * quantile_size), layers.Reshape((output_size, quantile_size, 1))] + for _ in range(output_size) + ] + + def get_config(self): + config = super(QRDQNOutput, self).get_config() + config.update({"quantile_size": self.quantile_size}) + + return config + + +__all__ = ["inputs_f", "Output", "DQNOutput", "QRDQNOutput"] \ No newline at end of file diff --git a/ejtraderRL/nn/layers/depthwiseconv1d.py b/ejtraderRL/nn/layers/depthwiseconv1d.py new file mode 100644 index 0000000..449b40e --- /dev/null +++ b/ejtraderRL/nn/layers/depthwiseconv1d.py @@ -0,0 +1,237 @@ +from tensorflow.python.framework import tensor_shape +from tensorflow.python.keras import constraints +from tensorflow.python.keras import initializers +from tensorflow.python.keras import regularizers +from tensorflow.python.keras.engine.input_spec import InputSpec +from tensorflow.python.keras.layers.convolutional import Conv1D +# imports for backwards namespace compatibility +# pylint: disable=unused-import +# pylint: enable=unused-import +from tensorflow.python.keras.utils import conv_utils +from tensorflow.python.ops import array_ops +from tensorflow.python.ops import nn + + +# pylint: disable=g-classes-have-attributes + + +class DepthwiseConv1D(Conv1D): + """Depthwise 1D convolution. + + Depthwise convolution is a type of convolution in which a single convolutional + filter is apply to each input channel (i.e. in a depthwise way). + You can understand depthwise convolution as being + the first step in a depthwise separable convolution. + + It is implemented via the following steps: + + - Split the input into individual channels. + - Convolve each input with the layer's kernel (called a depthwise kernel). + - Stack the convolved outputs together (along the channels axis). + + Unlike a regular 1D convolution, depthwise convolution does not mix + information across different input channels. + + The `depth_multiplier` argument controls how many + output channels are generated per input channel in the depthwise step. + + Args: + kernel_size: An integer or tuple/list of 2 integers, specifying the + height and width of the 1D convolution window. + Can be a single integer to specify the same value for + all spatial dimensions. + strides: An integer or tuple/list of 2 integers, + specifying the strides of the convolution along the height and width. + Can be a single integer to specify the same value for + all spatial dimensions. + Specifying any stride value != 1 is incompatible with specifying + any `dilation_rate` value != 1. + padding: one of `'valid'` or `'same'` (case-insensitive). + `"valid"` means no padding. `"same"` results in padding with zeros evenly + to the left/right or up/down of the input such that output has the same + height/width dimension as the input. + depth_multiplier: The number of depthwise convolution output channels + for each input channel. + The total number of depthwise convolution output + channels will be equal to `filters_in * depth_multiplier`. + data_format: A string, + one of `channels_last` (default) or `channels_first`. + The ordering of the dimensions in the inputs. + `channels_last` corresponds to inputs with shape + `(batch_size, height, width, channels)` while `channels_first` + corresponds to inputs with shape + `(batch_size, channels, height, width)`. + It defaults to the `image_data_format` value found in your + Keras config file at `~/.keras/keras.json`. + If you never set it, then it will be 'channels_last'. + dilation_rate: An integer or tuple/list of 2 integers, specifying + the dilation rate to use for dilated convolution. + Currently, specifying any `dilation_rate` value != 1 is + incompatible with specifying any `strides` value != 1. + activation: Activation function to use. + If you don't specify anything, no activation is applied ( + see `keras.activations`). + use_bias: Boolean, whether the layer uses a bias vector. + depthwise_initializer: Initializer for the depthwise kernel matrix ( + see `keras.initializers`). If None, the default initializer ( + 'glorot_uniform') will be used. + bias_initializer: Initializer for the bias vector ( + see `keras.initializers`). If None, the default initializer ( + 'zeros') will bs used. + depthwise_regularizer: Regularizer function applied to + the depthwise kernel matrix (see `keras.regularizers`). + bias_regularizer: Regularizer function applied to the bias vector ( + see `keras.regularizers`). + activity_regularizer: Regularizer function applied to + the output of the layer (its 'activation') ( + see `keras.regularizers`). + depthwise_constraint: Constraint function applied to + the depthwise kernel matrix ( + see `keras.constraints`). + bias_constraint: Constraint function applied to the bias vector ( + see `keras.constraints`). + + Input shape: + 3D tensor with shape: + `(batch_size, channels, steps)` if data_format='channels_first' + or 3D tensor with shape: + `(batch_size, steps, channels)` if data_format='channels_last'. + + Output shape: + 3D tensor with shape: + `(batch_size, filters, new_steps)` if data_format='channels_first' + or 3D tensor with shape: + `(batch_size, new_steps, filters)` if data_format='channels_last'. + `new_steps` value might have changed due to padding or strides. + + Returns: + A tensor of rank 3 representing + `activation(separableconv1d(inputs, kernel) + bias)`. + + Raises: + ValueError: when both `strides` > 1 and `dilation_rate` > 1. + """ + + def __init__(self, + kernel_size, + strides=1, + padding='valid', + depth_multiplier=1, + data_format=None, + dilation_rate=1, + activation=None, + use_bias=True, + depthwise_initializer='glorot_uniform', + bias_initializer='zeros', + depthwise_regularizer=None, + bias_regularizer=None, + activity_regularizer=None, + depthwise_constraint=None, + bias_constraint=None, + **kwargs): + super(DepthwiseConv1D, self).__init__( + filters=None, + kernel_size=kernel_size, + strides=strides, + padding=padding, + data_format=data_format, + dilation_rate=dilation_rate, + activation=activation, + use_bias=use_bias, + bias_regularizer=bias_regularizer, + activity_regularizer=activity_regularizer, + bias_constraint=bias_constraint, + **kwargs) + self.depth_multiplier = depth_multiplier + self.depthwise_initializer = initializers.get(depthwise_initializer) + self.depthwise_regularizer = regularizers.get(depthwise_regularizer) + self.depthwise_constraint = constraints.get(depthwise_constraint) + self.bias_initializer = initializers.get(bias_initializer) + + def build(self, input_shape): + input_shape = tensor_shape.TensorShape(input_shape) + channel_axis = self._get_channel_axis() + if input_shape.dims[channel_axis].value is None: + raise ValueError('The channel dimension of the inputs ' + 'should be defined. Found `None`.') + input_dim = int(input_shape[channel_axis]) + self.input_spec = InputSpec(ndim=self.rank + 2, + axes={channel_axis: input_dim}) + depthwise_kernel_shape = self.kernel_size + (input_dim, + self.depth_multiplier) + + self.depthwise_kernel = self.add_weight( + shape=depthwise_kernel_shape, + initializer=self.depthwise_initializer, + name='depthwise_kernel', + regularizer=self.depthwise_regularizer, + constraint=self.depthwise_constraint) + + if self.use_bias: + self.bias = self.add_weight(shape=(input_dim * self.depth_multiplier,), + initializer=self.bias_initializer, + name='bias', + regularizer=self.bias_regularizer, + constraint=self.bias_constraint) + else: + self.bias = None + self.built = True + + def call(self, inputs): + if self.padding == 'causal': + inputs = array_ops.pad(inputs, self._compute_causal_padding(inputs)) + if self.data_format == 'channels_last': + strides = (1,) + self.strides * 2 + (1,) + spatial_start_dim = 1 + else: + strides = (1, 1) + self.strides * 2 + spatial_start_dim = 2 + + # Explicitly broadcast inputs and kernels to 4D. + inputs = array_ops.expand_dims(inputs, spatial_start_dim) + depthwise_kernel = array_ops.expand_dims(self.depthwise_kernel, 0) + dilation_rate = (1,) + self.dilation_rate + + if self.padding == 'causal': + op_padding = 'valid' + else: + op_padding = self.padding + op_padding = op_padding.upper() + + outputs = nn.depthwise_conv2d( + inputs, + depthwise_kernel, + strides=strides, + padding=op_padding, + rate=dilation_rate, + data_format=conv_utils.convert_data_format(self.data_format, ndim=4)) + + if self.use_bias: + outputs = nn.bias_add( + outputs, + self.bias, + data_format=conv_utils.convert_data_format(self.data_format, ndim=4)) + + outputs = array_ops.squeeze(outputs, [spatial_start_dim]) + + if self.activation is not None: + return self.activation(outputs) + return outputs + + def get_config(self): + config = super(DepthwiseConv1D, self).get_config() + config.pop('filters') + config.pop('kernel_initializer') + config.pop('kernel_regularizer') + config.pop('kernel_constraint') + config['depth_multiplier'] = self.depth_multiplier + config['depthwise_initializer'] = initializers.serialize( + self.depthwise_initializer) + config['depthwise_regularizer'] = regularizers.serialize( + self.depthwise_regularizer) + config['depthwise_constraint'] = constraints.serialize( + self.depthwise_constraint) + return config + + +__all__ = ["DepthwiseConv1D"] \ No newline at end of file diff --git a/ejtraderRL/nn/layers/lambdalayer.py b/ejtraderRL/nn/layers/lambdalayer.py new file mode 100644 index 0000000..4992c24 --- /dev/null +++ b/ejtraderRL/nn/layers/lambdalayer.py @@ -0,0 +1,83 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf + + +class LambdaLayer(layers.Layer): + + def __init__(self, out_dim, heads=4, use_bias=False, u=4, kernel_size=7, **kwargs): + super(LambdaLayer, self).__init__() + + self.out_dim = out_dim + k = 16 + self.heads = heads + self.v = out_dim // heads + self.u = u + self.kernel_size = kernel_size + self.use_bias = use_bias + + self.top_q = tf.keras.layers.Conv1D(k * heads, 1, 1, "same", use_bias=use_bias) + self.top_k = tf.keras.layers.Conv1D(k * u, 1, 1, "same", use_bias=use_bias) + self.top_v = tf.keras.layers.Conv1D(self.v * self.u, 1, 1, "same", use_bias=use_bias) + + self.norm_q = tf.keras.layers.LayerNormalization() + self.norm_v = tf.keras.layers.LayerNormalization() + + self.rearrange_q = Sequential([ + layers.Reshape((-1, heads, k)), + layers.Permute((2, 3, 1)) + ]) + self.rearrange_k = Sequential([ + layers.Reshape((-1, u, k)), + layers.Permute((2, 3, 1)) + ]) + self.rearrange_v = Sequential([ + layers.Reshape((-1, u, self.v)), + layers.Permute((2, 3, 1)) + ]) + + self.rearrange_v2 = layers.Permute((2, 3, 1)) + self.rearrange_lp = layers.Permute((1, 3, 2)) + self.rearrange_output = layers.Reshape((-1, out_dim)) + + self.pos_conv = tf.keras.layers.Conv2D(k, (1, self.kernel_size), padding="same") + + def call(self, inputs, *args, **kwargs): + q = self.top_q(inputs) + k = self.top_k(inputs) + v = self.top_v(inputs) + + q = self.norm_q(q) + v = self.norm_v(v) + + q = self.rearrange_q(q) + k = self.rearrange_k(k) + v = self.rearrange_v(v) + + k = tf.nn.softmax(k) + + lc = tf.einsum("b u k n, b u v n -> b k v", k, v) + yc = tf.einsum("b h k n, b k v -> b n h v", q, lc) + + v = self.rearrange_v2(v) + lp = self.pos_conv(v) + lp = self.rearrange_lp(lp) + yp = tf.einsum("b h k n, b v k n -> b n h v", q, lp) + + y = yc + yp + output = self.rearrange_output(y) + + return output + + def get_config(self): + config = { + "out_dim": self.out_dim, + "heads": self.heads, + "use_bias": self.use_bias, + "u": self.u, + "kernel_size": self.kernel_size + } + + return config + + +__all__ = ["LambdaLayer"] \ No newline at end of file diff --git a/ejtraderRL/nn/layers/pyconv.py b/ejtraderRL/nn/layers/pyconv.py new file mode 100644 index 0000000..8e3eeb3 --- /dev/null +++ b/ejtraderRL/nn/layers/pyconv.py @@ -0,0 +1,34 @@ +from tensorflow.keras import layers + + +class Pyconv(layers.Layer): + def __init__(self, dim, groups=32): + super(Pyconv, self).__init__() + + self.dim = dim + self.groups = groups + + self.k = k = [3, 5, 7, 9] + self.conv = [ + layers.Conv1D(dim // 4, k, 1, "same", kernel_initializer="he_normal", groups=groups) for k + in k + ] + self.concat = layers.Concatenate() + + def call(self, inputs, *args, **kwargs): + x = [] + for conv in self.conv: + x.append(conv(inputs)) + + return self.concat(x) + + def get_config(self): + config = { + "dim": self.dim, + "groups": self.groups + } + + return config + + +__all__ = ["Pyconv"] \ No newline at end of file diff --git a/ejtraderRL/nn/layers/skconv.py b/ejtraderRL/nn/layers/skconv.py new file mode 100644 index 0000000..0cf863a --- /dev/null +++ b/ejtraderRL/nn/layers/skconv.py @@ -0,0 +1,53 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf +import numpy as np + + +class SKConv(layers.Layer): + def __init__(self, filters: int, groups=32, r=16, **kwargs): + super(SKConv, self).__init__() + self.filters = filters + self.z_filters = np.maximum(filters // r, 32) + self.groups = groups + self.r = r + + self.u1 = layers.Conv1D(filters, 3, 1, "same", kernel_initializer="he_normal", groups=groups) + self.u2 = layers.Conv1D(filters, 5, 1, "same", kernel_initializer="he_normal", dilation_rate=1, groups=groups) + + self.add = layers.Add(axis=-1) + + self.z = layers.Dense(self.z_filters, "elu", kernel_initializer="he_normal") + + self.a = layers.Dense(self.filters) + self.b = layers.Dense(self.filters) + self.concat = layers.Concatenate(axis=1) + + def call(self, inputs, *args, **kwargs): + u1 = self.u1(inputs) + u2 = self.u2(inputs) + + u = self.add([u1, u2]) + s = tf.reduce_mean(u, axis=1, keepdims=True) + z = self.z(s) + a = self.a(z) + b = self.b(z) + ab = self.concat([a, b]) + ab = tf.nn.softmax(ab, axis=1) + a, b = tf.split(ab, 2, 1) + + u1 *= a + u2 *= b + + return u1 + u2 + + def get_config(self): + config = { + "filters": self.filters, + "groups": self.groups, + "r": self.r + } + + return config + + +__all__ = ["SKConv"] \ No newline at end of file diff --git a/ejtraderRL/nn/layers/transformer.py b/ejtraderRL/nn/layers/transformer.py new file mode 100644 index 0000000..d4b09f6 --- /dev/null +++ b/ejtraderRL/nn/layers/transformer.py @@ -0,0 +1,132 @@ +from tensorflow.keras import layers, Sequential +import tensorflow as tf + + +class PositionAdd(layers.Layer): + def build(self, input_shape): + self.pe = self.add_weight("pe", [input_shape[1], input_shape[2]], + initializer=tf.keras.initializers.zeros()) + + def call(self, inputs, **kwargs): + return inputs + self.pe + + +class MHSA(layers.Layer): + def __init__(self, dim, num_heads, use_bias=True, noise=layers.Dropout, noise_r=0, **kwargs): + super(MHSA, self).__init__(**kwargs) + self.num_heads = num_heads + self.dim = dim + self.noise = noise + self.noise_r = noise_r + self.use_bias = use_bias + + head_dim = dim // num_heads + self.scale = head_dim ** -0.5 + + self.qkv = layers.Dense(dim * 3, use_bias=use_bias) + self.qkv_reshape = layers.Reshape((-1, num_heads, head_dim)) + self.qv_permute = layers.Permute((2, 1, 3)) + self.k_permute = layers.Permute((2, 3, 1)) + self.attn_reshape = Sequential([ + layers.Permute((2, 1, 3)), + layers.Reshape((-1, dim)) + ]) + self.proj = layers.Dense(dim) + self.drop_out = noise(noise_r) + + def call(self, inputs: tf.Tensor, training=False, *args, **kwargs): + qkv = self.qkv(inputs) + q, k, v = tf.split(qkv, 3, -1) + + q = self.qkv_reshape(q) + k = self.qkv_reshape(k) + v = self.qkv_reshape(v) + + q = self.qv_permute(q) + k = self.k_permute(k) + v = self.qv_permute(v) + + attn = tf.matmul(q, k) * self.scale + attn = tf.nn.softmax(attn, axis=-1) + + x = tf.matmul(attn, v) + x = self.attn_reshape(x) + x = self.proj(x) + x = self.drop_out(x) + + return x + + def get_config(self): + config = { + "num_heads": self.num_heads, + "dim": self.dim, + "use_bias": self.use_bias, + "noise": self.noise, + "noise_r": self.noise_r + } + return config + + +class TransformerMlp(layers.Layer): + def __init__(self, dim, mlp_dim, noise=layers.Dropout, noise_r=0): + super(TransformerMlp, self).__init__() + self.dim = dim + self.mlp_dim = mlp_dim + self.noise = noise + self.noise_r = noise_r + + self.dense = Sequential([ + layers.Dense(mlp_dim, "gelu"), + noise(noise_r), + layers.Dense(dim), + noise(noise_r) + ]) + + def call(self, inputs, *args, **kwargs): + return self.dense(inputs) + + def get_config(self): + config = { + "dim": self.dim, + "mlp_dim": self.mlp_dim, + "noise": self.noise, + "noise_r": self.noise_r + } + + return config + + +class Transformer(layers.Layer): + def __init__(self, dim, mlp_dim, heads, use_bias=False, noise=layers.Dropout, noise_r=0): + super(Transformer, self).__init__() + self.dim = dim + self.mlp_dim = mlp_dim + self.heads = heads + self.use_bias = use_bias + + self.attn = Sequential([layers.LayerNormalization(), MHSA(dim, heads, use_bias, noise, noise_r)]) + self.mlp = Sequential([layers.LayerNormalization(), TransformerMlp(dim, mlp_dim, noise, noise_r)]) + + def call(self, inputs, *args, **kwargs): + x = self.attn(inputs) + inputs + x = self.mlp(x) + x + + return x + + def get_config(self): + config = { + "dim": self.dim, + "mlp_dim": self.mlp_dim, + "heads": self.heads, + "use_bias": self.use_bias + } + + return config + + +__all__ = [ + "PositionAdd", + "MHSA", + "TransformerMlp", + "Transformer" +] \ No newline at end of file diff --git a/ejtraderRL/nn/losses/__init__.py b/ejtraderRL/nn/losses/__init__.py new file mode 100644 index 0000000..9455de6 --- /dev/null +++ b/ejtraderRL/nn/losses/__init__.py @@ -0,0 +1 @@ +from .losses import * diff --git a/ejtraderRL/nn/losses/losses.py b/ejtraderRL/nn/losses/losses.py new file mode 100644 index 0000000..2ef6a47 --- /dev/null +++ b/ejtraderRL/nn/losses/losses.py @@ -0,0 +1,44 @@ +from tensorflow.keras.losses import Loss +import tensorflow as tf +import numpy as np + + +class DQNLoss(Loss): + k = 2 + + def call(self, q_backup, q): + k = self.k + + error = q_backup - q + loss = tf.where(tf.abs(error) <= k, error ** 2 * 0.5, 0.5 * k ** 2 + k * (tf.abs(error) - k)) + loss = tf.reduce_mean(loss) + + return loss + + +class QRDQNLoss(Loss): + def __init__(self, quantile_size=32): + super(QRDQNLoss, self).__init__() + self.quantile_size = quantile_size + + self.k = 1 + plus_tau = np.arange(1, quantile_size, dtype=np.float32) / quantile_size + self.plus_tau = np.reshape(plus_tau, (1, 1, 32, 1)) + self.minus_tau = np.abs(self.plus_tau - 1) + + def call(self, q_backup, q): + k = self.k + + error = q_backup - q + loss = tf.where(tf.abs(error) <= k, error ** 2 * 0.5, 0.5 * k ** 2 + k * (tf.abs(error) - k)) + loss = tf.where(error > 0, loss * self.plus_tau, loss * self.minus_tau) + loss = tf.reduce_mean(loss, (0, 1, 3)) + loss = tf.reduce_sum(loss) + + return loss + + def get_config(self): + return {"quantile_size": self.quantile_size} + + +__all__ = ["DQNLoss", "QRDQNLoss"] \ No newline at end of file diff --git a/ejtraderRL/nn/model/__init__.py b/ejtraderRL/nn/model/__init__.py new file mode 100644 index 0000000..8fefd2b --- /dev/null +++ b/ejtraderRL/nn/model/__init__.py @@ -0,0 +1 @@ +from .ej import * diff --git a/ejtraderRL/nn/model/ej.py b/ejtraderRL/nn/model/ej.py new file mode 100644 index 0000000..37fb566 --- /dev/null +++ b/ejtraderRL/nn/model/ej.py @@ -0,0 +1,62 @@ +from tensorflow.keras import Model +import tensorflow as tf + + +class EJModel(Model): + rho = 0.05 + + def train_step(self, data): + x, y = data + e_ws = [] + + with tf.GradientTape() as tape: + predictions = self(x, training=True) + loss = self.compiled_loss(y, predictions, regularization_losses=self.losses) + + if "_optimizer" in dir(self.optimizer): # mixed float policy + grads_and_vars = self.optimizer._optimizer._compute_gradients(loss, var_list=self.trainable_variables, + tape=tape) + else: + grads_and_vars = self.optimizer._compute_gradients(loss, var_list=self.trainable_variables, tape=tape) + + grads = [g for g, _ in grads_and_vars] + + grad_norm = self._grad_norm(grads) + scale = self.rho / (grad_norm + 1e-12) + + for (grad, param) in zip(grads, self.trainable_variables): + e_w = grad * scale + e_ws.append(e_w) + param.assign_add(e_w) + + with tf.GradientTape() as tape: + predictions = self(x, training=True) + loss = self.compiled_loss(y, predictions, regularization_losses=self.losses) + + if "_optimizer" in dir(self.optimizer): # mixed float policy + grads_and_vars = self.optimizer._optimizer._compute_gradients(loss, var_list=self.trainable_variables, + tape=tape) + else: + grads_and_vars = self.optimizer._compute_gradients(loss, var_list=self.trainable_variables, tape=tape) + grads = [g for g, _ in grads_and_vars] + + for e_w, param in zip(e_ws, self.trainable_variables): + param.assign_sub(e_w) + + grads_and_vars = list(zip(grads, self.trainable_variables)) + + self.optimizer.apply_gradients(grads_and_vars) + self.compiled_metrics.update_state(y, predictions) + + return {m.name: m.result() for m in self.metrics} + + def _grad_norm(self, gradients): + norm = tf.norm( + tf.stack([ + tf.norm(grad) for grad in gradients if grad is not None + ]) + ) + return norm + + +__all__ = ["EJModel", "Model"] \ No newline at end of file diff --git a/examples/train.py b/examples/train.py new file mode 100644 index 0000000..3c5594d --- /dev/null +++ b/examples/train.py @@ -0,0 +1,30 @@ +from ejtraderRL import data, agent + +# forex data +df = data.get_forex_data("EURUSD", "h1") +# stoch data +#df = data.get_stock_data("AAPL") + +agent = agent.DQN(df=df, model_name="efficientnet_b0", lr=1e-4, pip_scale=25, n=3, use_device="cpu", + gamma=0.99, train_spread=0.2, balance=1000, spread=0.7, risk=0.01) + + +""" +:param df: pandas dataframe or csv file. Must contain open, low, high, close +:param lr: learning rate +:param model_name: None or model name, If None -> model is not created. +:param pip_scale: Controls the degree of overfitting +:param n: int +:param use_device: tpu or gpu or cpu +:param gamma: float +:param train_spread: Determine the degree of long-term training. The smaller the value, the more short-term the trade. +:param balance: Account size +:param spread: Cost of Trade +:param risk: What percentage of the balance is at risk +""" + +agent.train() + +print("Saving model") + +agent.model.save("efficientnet_b0_s0_H1") \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2176b9e --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +from setuptools import setup +from codecs import open +from os import path +from setuptools import find_packages + +here = path.abspath(path.dirname(__file__)) + + +with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='ejtraderRL', + packages=find_packages(), + + version='1.0.0', + + license='Apache-2.0 License', + + install_requires=['numpy', "ta", "pandas", "pandas_datareader", "matplotlib","IPython"], + + author='traderpedroso', + author_email='info@ejtrader.com', + + url='https://github.com/ejejtraderRLabs/', + + description='Reinforcement learning Trading envoriments.', + long_description=long_description, + long_description_content_type='text/markdown', + keywords='ejtraderRL', + +)