From 24b17bf9669ff09a3711c840af725221495c007c Mon Sep 17 00:00:00 2001 From: Peter Dekkers Date: Sat, 6 Apr 2024 11:44:34 +0200 Subject: [PATCH] better multi strategy support --- .gitignore | 2 +- pyproject.toml | 4 ++-- roboquant/feeds/__init__.py | 1 + roboquant/feeds/feed.py | 3 +++ roboquant/ml/__init__.py | 8 ------- roboquant/ml/envs.py | 3 +++ roboquant/run.py | 16 ++++++------- roboquant/strategies/emacrossover.py | 33 +++++++++++++++++++++++++++ roboquant/strategies/multistrategy.py | 33 ++++++++++++++++++--------- samples/alpaca_feed.py | 5 +--- samples/tensorboard_metrics.py | 11 +++++---- 11 files changed, 80 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 7bb611b..926d417 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ __pycache__ runs/ .idea/ *.db -scratch*.py +scratch/ dist/ roboquant.egg-info/ build/ diff --git a/pyproject.toml b/pyproject.toml index 626d935..d38b2c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ testpaths = [ [tool.pyright] reportOptionalOperand = "none" -exclude = ["samples/*.py"] +exclude = ["samples/*.py", "scratch/*.py"] [tool.pylint.MASTER] ignore-paths = 'samples' @@ -27,7 +27,7 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] -exclude = ["docs*", "tests*", "samples*"] +exclude = ["docs*", "tests*", "samples*", "scratch*"] [tool.setuptools.package-data] "*" = ["*.json"] diff --git a/roboquant/feeds/__init__.py b/roboquant/feeds/__init__.py index 3fecc7d..92449d9 100644 --- a/roboquant/feeds/__init__.py +++ b/roboquant/feeds/__init__.py @@ -7,6 +7,7 @@ from .randomwalk import RandomWalk from .sqllitefeed import SQLFeed from .tiingo import TiingoLiveFeed, TiingoHistoricFeed +from .feedutil import get_sp500_symbols try: from .alpacafeed import AlpacaLiveFeed diff --git a/roboquant/feeds/feed.py b/roboquant/feeds/feed.py index f833ec2..bb3e42f 100644 --- a/roboquant/feeds/feed.py +++ b/roboquant/feeds/feed.py @@ -24,6 +24,9 @@ def play(self, channel: EventChannel): """ ... + def timeframe(self) -> Timeframe | None: + return None + def play_background(self, timeframe: Timeframe | None = None, channel_capacity: int = 10) -> EventChannel: """ Plays this feed in the background on its own thread. diff --git a/roboquant/ml/__init__.py b/roboquant/ml/__init__.py index 14afca5..e69de29 100644 --- a/roboquant/ml/__init__.py +++ b/roboquant/ml/__init__.py @@ -1,8 +0,0 @@ -try: - from gymnasium.envs.registration import register - - register(id="roboquant/StrategyEnv-v0", entry_point="roboquant.ml.envs:StrategyEnv") - register(id="roboquant/TraderEnv-v0", entry_point="roboquant.ml.envs:TraderEnv") - -except ImportError: - pass diff --git a/roboquant/ml/envs.py b/roboquant/ml/envs.py index f05d2df..f76118f 100644 --- a/roboquant/ml/envs.py +++ b/roboquant/ml/envs.py @@ -2,6 +2,7 @@ import logging import gymnasium as gym from gymnasium import spaces +from gymnasium.envs.registration import register import numpy as np from numpy.typing import NDArray from roboquant.account import Account @@ -19,6 +20,8 @@ from roboquant.traders.trader import Trader +register(id="roboquant/StrategyEnv-v0", entry_point="roboquant.ml.envs:StrategyEnv") +register(id="roboquant/TraderEnv-v0", entry_point="roboquant.ml.envs:TraderEnv") logger = logging.getLogger(__name__) diff --git a/roboquant/run.py b/roboquant/run.py index 4f2c84b..fb6b77a 100644 --- a/roboquant/run.py +++ b/roboquant/run.py @@ -10,14 +10,14 @@ def run( - feed: Feed, - strategy: Strategy | None = None, - trader: Trader | None = None, - broker: Broker | None = None, - journal: Journal | None = None, - timeframe: Timeframe | None = None, - capacity: int = 10, - heartbeat_timeout: float | None = None + feed: Feed, + strategy: Strategy | None = None, + trader: Trader | None = None, + broker: Broker | None = None, + journal: Journal | None = None, + timeframe: Timeframe | None = None, + capacity: int = 10, + heartbeat_timeout: float | None = None, ) -> Account: """Start a new run. diff --git a/roboquant/strategies/emacrossover.py b/roboquant/strategies/emacrossover.py index 8dc9794..b612a17 100644 --- a/roboquant/strategies/emacrossover.py +++ b/roboquant/strategies/emacrossover.py @@ -53,3 +53,36 @@ def add_price(self, price: float): self.price2 = m2 * self.price2 + (1.0 - m2) * price self.step += 1 return self.step + + +class _Calculator2: + + __slots__ = "entries", "step" + + def __init__(self, *momentums, price): + self.entries = [[m, price] for m in momentums] + self.step = 0 + + def is_above(self): + prev = None + for _, p in self.entries: + if prev is not None and p <= prev: + return False + prev = p + return True + + def is_below(self): + prev = None + for _, p in self.entries: + if prev is not None and p >= prev: + return False + prev = p + return True + + def add_price(self, price: float): + for entry in self.entries: + m, p = entry + entry[1] = m * p + (1 - m) * price + + self.step += 1 + return self.step diff --git a/roboquant/strategies/multistrategy.py b/roboquant/strategies/multistrategy.py index def6836..18531fe 100644 --- a/roboquant/strategies/multistrategy.py +++ b/roboquant/strategies/multistrategy.py @@ -1,33 +1,44 @@ from typing import Literal +from itertools import groupby +from statistics import mean from roboquant.event import Event +from roboquant.signal import Signal from roboquant.strategies.strategy import Strategy class MultiStrategy(Strategy): """Combine one or more strategies. The MultiStrategy provides additional control on how to handle conflicting - signals for the same symbols: + signals for the same symbols via the signal_filter: - - first: in case of multiple signals for a symbol, the first strategy wins - - last: in case of multiple signals for a symbol, the last strategy wins. This is also the default policy + - first: in case of multiple signals for the same symbol, the first one wins + - last: in case of multiple signals for the same symbol, the last one wins. + - avg: return the avgerage of the signals. All signals will be ENTRY and EXIT. + - none: return all signals. This is also the default. """ - def __init__(self, *strategies: Strategy, policy: Literal["last", "first", "all"] = "last"): + def __init__(self, *strategies: Strategy, signal_filter: Literal["last", "first", "avg", "none"] = "none"): self.strategies = list(strategies) - self.policy = policy + self.signal_filter = signal_filter def create_signals(self, event: Event): - signals = [] + signals: list[Signal] = [] for strategy in self.strategies: - tmp = strategy.create_signals(event) - signals += tmp + signals += strategy.create_signals(event) - match self.policy: + match self.signal_filter: + case "none": + return signals case "last": s = {s.symbol: s for s in signals} return list(s.values()) case "first": s = {s.symbol: s for s in reversed(signals)} return list(s.values()) - case "all": - return signals + case "avg": + result = [] + g = groupby(signals, lambda x: x.symbol) + for symbol, v in g: + rating = mean(s.rating for s in v) + result.append(Signal(symbol, rating)) + return result diff --git a/samples/alpaca_feed.py b/samples/alpaca_feed.py index e5a11a8..45838a0 100644 --- a/samples/alpaca_feed.py +++ b/samples/alpaca_feed.py @@ -1,9 +1,6 @@ # %% from datetime import timedelta -from roboquant.feeds.aggregate import AggregatorFeed -from roboquant.feeds.alpacafeed import AlpacaLiveFeed -from roboquant.feeds.feedutil import get_sp500_symbols - +from roboquant.feeds import AggregatorFeed, AlpacaLiveFeed, get_sp500_symbols # %% alpaca_feed = AlpacaLiveFeed() diff --git a/samples/tensorboard_metrics.py b/samples/tensorboard_metrics.py index c87e2d9..51b9216 100644 --- a/samples/tensorboard_metrics.py +++ b/samples/tensorboard_metrics.py @@ -5,15 +5,16 @@ from roboquant.journals import TensorboardJournal, PNLMetric, RunMetric, FeedMetric, PriceItemMetric, AlphaBeta # %% -# Compare 3 runs with different parameters using tensorboard -feed = rq.feeds.YahooFeed("JPM", "IBM", "F", start_date="2000-01-01") +# Compare runs with different parameters using tensorboard +feed = rq.feeds.YahooFeed("JPM", "IBM", "F", "MSFT", "V", "GE","CSCO", "WMT", "XOM", "INTC", start_date="2010-01-01") -params = [(3, 5), (13, 26), (12, 50)] +hyper_params = [(3, 5), (13, 26), (12, 50)] -for p1, p2 in params: +for p1, p2 in hyper_params: s = rq.strategies.EMACrossover(p1, p2) log_dir = f"""runs/ema_{p1}_{p2}""" writer = Writer(log_dir) journal = TensorboardJournal(writer, PNLMetric(), RunMetric(), FeedMetric(), PriceItemMetric("JPM"), AlphaBeta(200)) - rq.run(feed, s, journal=journal) + account = rq.run(feed, s, journal=journal) + print(p1, p2, account.equity()) writer.close()