Skip to content

Commit

Permalink
Improved Signal Strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaron committed Jun 15, 2024
1 parent cbc329e commit aad4916
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 19 deletions.
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.4.0"
__version__ = "0.4.1"

from roboquant import brokers
from roboquant import feeds
Expand Down
38 changes: 20 additions & 18 deletions roboquant/strategies/signalstrategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from roboquant.order import Order
from roboquant.strategies.signal import Signal
from roboquant.strategies.strategy import Strategy
from ..account import Account
from ..event import PriceItem
from roboquant.account import Account


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -53,7 +53,7 @@ def __init__(self, signal: Signal, position: Decimal) -> None:

def log(self, rule: str, **kwargs):
if logger.isEnabledFor(logging.INFO):
extra = ' '.join(f"{k}={v}" for k, v in kwargs.items())
extra = " ".join(f"{k}={v}" for k, v in kwargs.items())
logger.info(
"Discarded signal because %s [symbol=%s rating=%s type=%s position=%s %s]",
rule,
Expand All @@ -66,6 +66,7 @@ def log(self, rule: str, **kwargs):


class SignalStrategy(Strategy):
# pylint: disable=too-many-instance-attributes
"""Implementation of a Stategy that has configurable rules to modify which signals are converted into orders.
This implementation will not generate orders if there is not a price in the event for the underlying symbol.
Expand All @@ -79,7 +80,8 @@ class SignalStrategy(Strategy):
- max_order_perc: the max percentage of the equity to allocate to a new order, default is 0.05 (5%)
- min_order_perc: the min percentage of the equity to allocate to a new order, default is 0.02 (2%)
- shorting: allow orders that could result in a short position, default is false
- price_type: the price type to use when determining order value, for example "CLOSE". Default is "DEFAULT"
- ask_price_type: the price type to use when determining order value, for example "CLOSE". Default is "DEFAULT"
- bid_price_type: the price type to use when determining order value, for example "CLOSE". Default is "DEFAULT"
It might be sometimes challenging to understand wby a signal isn't converted into an order. The flex-trader logs
at INFO level when certain rules have been fired.
Expand All @@ -98,9 +100,11 @@ def __init__(
max_order_perc=0.05,
min_order_perc=0.02,
max_position_perc=0.1,
price_type="DEFAULT",
ask_price_type="DEFAULT",
bid_price_type="DEFAULT",
shuffle_signals=False,
order_valid_for=timedelta(days=3)
order_valid_for=timedelta(days=3),
limit_perc=0.01,
) -> None:
super().__init__()
self.one_order_only = one_order_only
Expand All @@ -110,9 +114,11 @@ def __init__(
self.max_order_perc = max_order_perc
self.min_order_perc = min_order_perc
self.max_position_perc = max_position_perc
self.price_type = price_type
self.ask_price_type = ask_price_type
self.bid_price_type = bid_price_type
self.shuffle_signals = shuffle_signals
self.order_valid_for = order_valid_for
self.limit_perc = limit_perc

def _get_order_size(self, rating: float, contract_price: float, max_order_value: float) -> Decimal:
"""Return the order size"""
Expand All @@ -122,9 +128,7 @@ def _get_order_size(self, rating: float, contract_price: float, max_order_value:

@abstractmethod
def create_signals(self, event: Event) -> list[Signal]:
"""Create a signal for zero or more symbols. Signals are returned as a dictionary with key being the symbol and
the value being the Signal.
"""
"""Create signals for zero or more symbols. Signals are returned as a list."""
...

def create_orders(self, event: Event, account: Account) -> list[Order]:
Expand Down Expand Up @@ -163,7 +167,7 @@ def create_orders(self, event: Event, account: Account) -> list[Order]:
ctx.log("no price is available")
continue

price = item.price(self.price_type)
price = item.price(self.ask_price_type) if signal.is_buy else item.price(self.bid_price_type)

if not self.shorting and change == _PositionChange.ENTRY_SHORT:
ctx.log("no shorting")
Expand All @@ -179,7 +183,7 @@ def create_orders(self, event: Event, account: Account) -> list[Order]:
if rounded_size.is_zero():
ctx.log("cannot exit with order size zero")
continue
new_orders = self._get_orders(symbol, rounded_size, item, signal, event.time)
new_orders = self._get_orders(symbol, rounded_size, price, signal, event.time)
orders += new_orders
else:
if available < 0:
Expand Down Expand Up @@ -222,20 +226,18 @@ def create_orders(self, event: Event, account: Account) -> list[Order]:
)
continue

new_orders = self._get_orders(symbol, order_size, item, signal, event.time)
new_orders = self._get_orders(symbol, order_size, price, signal, event.time)
if new_orders:
orders += new_orders
available -= order_value

return orders

def _get_orders(self, symbol: str, size: Decimal, item: PriceItem, signal: Signal, time: datetime) -> list[Order]:
def _get_orders(self, symbol: str, size: Decimal, price: float, signal: Signal, time: datetime) -> list[Order]:
# pylint: disable=unused-argument
"""Return zero or more orders for the provided symbol and size.
"""Return zero or more orders for the provided symbol and size."""

Default is a MarketOrder. Overwrite this method to create different order types.
"""
limit = item.price(self.price_type)
limit = price * (1.0 + self.limit_perc) if size > 0 else price * (1.0 - self.limit_perc)
gtd = time + self.order_valid_for
return [Order(symbol, size, limit, gtd)]

Expand Down

0 comments on commit aad4916

Please sign in to comment.