Skip to content

Commit

Permalink
small cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaron committed Mar 22, 2024
1 parent ed440d8 commit 244c777
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 56 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ torch = ["torch>=2.1.0", "tensorboard>=2.15.2", "stable-baselines3[extra]>=2.2.1
yahoo = ["yfinance~=0.2.36"]
ibkr = ["nautilus-ibapi~=10.19.2"]
alpaca = ["alpaca-py"]
all = ["roboquant[torch,yahoo,ibkr]"]
all = ["roboquant[torch,yahoo,ibkr,alpaca]"]


[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion roboquant/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.2.8"
__version__ = "0.2.9"

from roboquant import brokers
from roboquant import feeds
Expand Down
20 changes: 13 additions & 7 deletions roboquant/brokers/simbroker.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class SimBroker(Broker):
def __init__(
self,
initial_deposit=1_000_000.0,
price_type="DEFAULT",
price_type="OPEN",
slippage=0.001,
clean_up_orders=True,
):
Expand Down Expand Up @@ -115,8 +115,10 @@ def _get_fill(self, order, price) -> Decimal:
The default implementation is:
- A market order is always fully filled,
- A limit order only when the limit is below the BUY price or
above the SELL price."""
- A limit order only when the limit is below the BUY price or above the SELL price.
Overwrite this method in a subclass if you require more advanced behavior, like partial fills.
"""
if order.limit is None:
return order.remaining
if order.is_buy and price <= order.limit:
Expand All @@ -127,7 +129,9 @@ def _get_fill(self, order, price) -> Decimal:
return Decimal(0)

def place_orders(self, orders):
"""Place new orders at this broker. The order gets assigned a unique id if it hasn't one already.
"""Place new orders at this broker. The order gets assigned a unique order-id if it hasn't one already.
Orders that are placed that have already an order-id are either update- or cancellation-orders.
There is no trading simulation yet performed or account updated. Orders placed at time `t`, will be
processed during time `t+1`. This protects against future bias.
Expand Down Expand Up @@ -180,7 +184,8 @@ def _process_create_orders(self, prices):
order.status = OrderStatus.FILLED

def sync(self, event: Event | None = None) -> Account:
"""This will perform the trading simulation for open orders areturn an updated the account"""
"""This will perform the order-execution simulation for the open orders and
return the updated the account as a result."""

acc = self._account
if event:
Expand All @@ -189,8 +194,9 @@ def sync(self, event: Event | None = None) -> Account:
prices = event.price_items if event else {}

if self.clean_up_orders:
# remove all the closed orders from the previous step
self._create_orders = {order_id: order for order_id, order in self._create_orders.items() if not order.is_closed}
# only keep the open orders from the previous step
# this improces performance a lot for large back tests
self._create_orders = {order_id: order for order_id, order in self._create_orders.items() if order.is_open}

self._process_modify_order()
self._process_create_orders(prices)
Expand Down
6 changes: 5 additions & 1 deletion roboquant/feeds/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
from .randomwalk import RandomWalk
from .sqllitefeed import SQLFeed
from .tiingo import TiingoLiveFeed, TiingoHistoricFeed
from .alpacafeed import AlpacaLiveFeed

try:
from .alpacafeed import AlpacaLiveFeed
except ImportError:
pass

try:
from .yahoo import YahooFeed
Expand Down
9 changes: 7 additions & 2 deletions roboquant/ml/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from gymnasium.envs.registration import register
try:
from gymnasium.envs.registration import register

register(id="roboquant/TradingStrategy-v0", entry_point="roboquant.ml.envs:StrategyEnv")
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
46 changes: 3 additions & 43 deletions roboquant/ml/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
import numpy as np
from numpy.typing import NDArray

from roboquant.signal import Signal
from roboquant.account import Account
from roboquant.event import Event, Bar
from roboquant.feeds.feed import Feed
from roboquant.strategies.strategy import Strategy


class Feature(ABC):
"""Features allows to create features from an event or account object.
Many features also allow post transformation after that, like normalization.
"""

@abstractmethod
def calc(self, evt: Event, account: Account | None) -> NDArray:
Expand Down Expand Up @@ -525,43 +525,3 @@ def calc(self, evt, account):
def size(self) -> int:
return 7


class FeatureStrategy(Strategy):
"""Abstract base class for strategies wanting to use features
for their input and target.
"""

def __init__(self, input_feature: Feature, label_feature: Feature, history: int, dtype="float32"):
self._features_x = []
self._features_y = []
self.input_feature = input_feature
self.label_feature = label_feature
self._hist = deque(maxlen=history)
self._dtype = dtype

def create_signals(self, event: Event) -> dict[str, Signal]:
h = self._hist
row = self.input_feature.calc(event, None)
h.append(row)
if len(h) == h.maxlen:
x = np.asarray(h, dtype=self._dtype)
return self.predict(x)
return {}

@abstractmethod
def predict(self, x: NDArray) -> dict[str, Signal]: ...

def _get_xy(self, feed: Feed, timeframe=None, warmup=0) -> tuple[NDArray, NDArray]:
channel = feed.play_background(timeframe)
x = []
y = []
while evt := channel.get():
if warmup:
self.label_feature.calc(evt, None)
self.input_feature.calc(evt, None)
warmup -= 1
else:
x.append(self.input_feature.calc(evt, None))
y.append(self.label_feature.calc(evt, None))

return np.asarray(x, dtype=self._dtype), np.asarray(y, dtype=self._dtype)
45 changes: 45 additions & 0 deletions roboquant/ml/strategies.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from abc import abstractmethod
from collections import deque
import numpy as np
from numpy.typing import NDArray

from roboquant.event import Event
from roboquant.ml.envs import Action2Signals, StrategyEnv, TraderEnv
from roboquant.ml.features import Feature
from roboquant.order import Order
Expand Down Expand Up @@ -57,3 +61,44 @@ def reset(self):
super().reset()
self.state = None
self.obs_feature.reset()


class FeatureStrategy(Strategy):
"""Abstract base class for strategies wanting to use features
for their input and target.
"""

def __init__(self, input_feature: Feature, label_feature: Feature, history: int, dtype="float32"):
self._features_x = []
self._features_y = []
self.input_feature = input_feature
self.label_feature = label_feature
self._hist = deque(maxlen=history)
self._dtype = dtype

def create_signals(self, event: Event) -> dict[str, Signal]:
h = self._hist
row = self.input_feature.calc(event, None)
h.append(row)
if len(h) == h.maxlen:
x = np.asarray(h, dtype=self._dtype)
return self.predict(x)
return {}

@abstractmethod
def predict(self, x: NDArray) -> dict[str, Signal]: ...

def _get_xy(self, feed, timeframe=None, warmup=0) -> tuple[NDArray, NDArray]:
channel = feed.play_background(timeframe)
x = []
y = []
while evt := channel.get():
if warmup:
self.label_feature.calc(evt, None)
self.input_feature.calc(evt, None)
warmup -= 1
else:
x.append(self.input_feature.calc(evt, None))
y.append(self.label_feature.calc(evt, None))

return np.asarray(x, dtype=self._dtype), np.asarray(y, dtype=self._dtype)
3 changes: 2 additions & 1 deletion roboquant/ml/torch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import torch
from torch.utils.data import DataLoader, Dataset

from roboquant.ml.strategies import FeatureStrategy
from roboquant.signal import Signal, BUY, SELL
from roboquant.ml.features import Feature, FeatureStrategy, NormalizeFeature
from roboquant.ml.features import Feature, NormalizeFeature

logger = logging.getLogger(__name__)

Expand Down

0 comments on commit 244c777

Please sign in to comment.