Skip to content

Commit

Permalink
removed one more dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaron committed Feb 23, 2024
1 parent 55b0f21 commit bc60038
Show file tree
Hide file tree
Showing 22 changed files with 210 additions and 347 deletions.
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ classifiers = [
keywords = ["trading", "investment", "finance", "crypto", "stocks", "exchange", "forex"]
dependencies = [
"numpy>=1.25.2",
"prettytable~=3.9.0",
"websocket-client~=1.7.0",
"requests>=2.31.0",
]
Expand Down
2 changes: 1 addition & 1 deletion roboquant/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from roboquant.brokers import Broker, SimBroker
from roboquant.traders import Trader, FlexTrader
from roboquant.trackers import Tracker, StandardTracker, BasicTracker, CAPMTracker, EquityTracker, TensorboardTracker
from roboquant.trackers import Tracker, BasicTracker, AlphaBetaTracker, EquityTracker, TensorboardTracker, MarketTracker
from roboquant.strategies import (
Strategy,
EMACrossover,
Expand Down
45 changes: 22 additions & 23 deletions roboquant/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from datetime import datetime
from decimal import Decimal
from roboquant.order import Order
from prettytable import PrettyTable


@dataclass(slots=True, frozen=True)
Expand All @@ -28,12 +27,18 @@ class Account:
Only the broker updates the state of the account and does this only during its `sync` method.
"""

buying_power: float
positions: dict[str, Position]
orders: list[Order]
last_update: datetime
equity: float

def __init__(self):
self.buying_power: float = 0.0
self.positions: dict[str, Position] = {}
self.orders: list[Order] = []
self.last_update: datetime = datetime.fromisoformat("1900-01-01T00:00:00+00:00")
self.equity = 0.0
self.equity: float = 0.0

def contract_value(self, symbol: str, size: Decimal, price: float) -> float:
"""Return the total value of the provided contract size denoted in the base currency of the account.
Expand All @@ -50,6 +55,7 @@ def mkt_value(self, prices: dict[str, float]) -> float:
return sum([self.contract_value(symbol, pos.size, prices[symbol]) for symbol, pos in self.positions.items()], 0.0)

def unrealized_pnl(self, prices: dict[str, float]) -> float:
"""Return the unrealized profit and loss for the open position given the provided market prices"""
return sum(
[self.contract_value(symbol, pos.size, prices[symbol] - pos.avg_price) for symbol, pos in self.positions.items()],
0.0,
Expand All @@ -64,6 +70,7 @@ def has_open_order(self, symbol: str) -> bool:
return False

def get_position_size(self, symbol) -> Decimal:
"""Return the position size for the symbol"""
pos = self.positions.get(symbol)
return pos.size if pos else Decimal(0)

Expand All @@ -72,27 +79,19 @@ def open_orders(self):
return [order for order in self.orders if not order.closed]

def __repr__(self) -> str:
p = PrettyTable(["account", "value"], align="r", float_format="12.2")
p.add_row(["buying power", self.buying_power])
p.add_row(["equity", self.equity])
p.add_row(["positions", len(self.positions)])
p.add_row(["orders", len(self.orders)])
p.add_row(["last update", self.last_update.strftime("%Y-%m-%d %H:%M:%S")])
result = p.get_string() + "\n\n"

if self.positions:
p = PrettyTable(["symbol", "position size", "avg price"], align="r", float_format="12.2")
for symbol, pos in self.positions.items():
p.add_row([symbol, pos.size, pos.avg_price])
result += p.get_string() + "\n\n"

if self.orders:
p = PrettyTable(["symbol", "order size", "order id", "limit", "status", "closed"], align="r", float_format="12.2")
for order in self.orders:
p.add_row([order.symbol, order.size, order.id, order.limit, order.status.name, order.closed])
result += p.get_string() + "\n"

return result
p = [f"{v.size}@{k}" for k, v in self.positions.items()]
p_str = ", ".join(p)

o = [f"{o.size}@{o.symbol}" for o in self.open_orders()]
o_str = ", ".join(o)

return f"""
buying power : {self.buying_power:_.2f}
equity : {self.equity:_.2f}
positions : {p_str}
open orders : {o_str}
last update : {self.last_update}
"""


class OptionAccount(Account):
Expand Down
25 changes: 21 additions & 4 deletions roboquant/order.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import dataclass
from decimal import Decimal
from enum import Flag, auto
from copy import copy
Expand Down Expand Up @@ -31,13 +32,25 @@ def closed(self):
"""Return True is the status is closed, False otherwise"""
return self in OrderStatus._CLOSE

def __repr__(self) -> str:
return self.name


@dataclass
class Order:
"""
A trading order. Default is a market order when only the size th specified. But optional a limit can be specified,
making it a limit order. The id is automatically assigned by the broker and should not be set manually.
making it a limit order.
The id is automatically assigned by the broker and should not be set manually.
"""

symbol: str
size: Decimal
limit: float | None
id: str | None
status: OrderStatus

def __init__(self, symbol: str, size: Decimal | str | int | float, limit: float | None = None):
self.symbol = symbol
self.size = Decimal(size)
Expand All @@ -58,7 +71,7 @@ def closed(self) -> bool:
return self.status.closed

def cancel(self) -> "Order":
"""Cancel this order. You can only cancel orders that are still open and have an id.
"""Create a cancellation order. You can only cancel orders that are still open and have an id.
The returned order looks like a regular order, but with its size set to zero.
"""
assert self.id is not None, "Can only cancel orders with an id"
Expand All @@ -69,8 +82,10 @@ def cancel(self) -> "Order":
return result

def update(self, size: Decimal | str | int | float | None = None, limit: float | None = None) -> "Order":
"""Update this order. You can only update orders that are still open and have an id.
You can update the size and/or limit of an order. The id of the order stays the same as the original order.
"""Create an update-order. You can update the size and/or limit of an order. The returned order has the same id
as the original order.
You can only update existing orders that are still open and have an id.
"""

assert self.id is not None, "Can only update orders with an id"
Expand Down Expand Up @@ -100,8 +115,10 @@ def is_cancellation(self):

@property
def is_buy(self):
"""Return True if this is a BUY order, False otherwise"""
return self.size > 0

@property
def is_sell(self):
"""Return True if this is a SELL order, False otherwise"""
return self.size < 0
2 changes: 1 addition & 1 deletion roboquant/roboquant.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@ def run(
orders = self.trader.create_orders(signals, event, account)
self.broker.place_orders(*orders)
if tracker:
tracker.log(event, account, signals, orders)
tracker.trace(event, account, signals, orders)

return self.broker.sync()
2 changes: 1 addition & 1 deletion roboquant/timeframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def split(self, n: int | timedelta) -> list["Timeframe"]:
period = n if isinstance(n, timedelta) else self.duration / n
end = self.start
result = []
while end in self:
while end < self.end:
start = end
end = start + period
result.append(Timeframe(start, end, False))
Expand Down
6 changes: 3 additions & 3 deletions roboquant/trackers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .tracker import Tracker
from roboquant.trackers.standardtracker import StandardTracker
from roboquant.trackers.capmtracker import CAPMTracker
from roboquant.trackers.tracker import Tracker
from roboquant.trackers.alphabeta import AlphaBetaTracker
from roboquant.trackers.basictracker import BasicTracker
from roboquant.trackers.equitytracker import EquityTracker
from roboquant.trackers.tensorboardtracker import TensorboardTracker
from roboquant.trackers.markettracker import MarketTracker
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from typing import Tuple
import numpy as np
from prettytable import PrettyTable

from roboquant.event import Event
from roboquant.timeframe import Timeframe
from .tracker import Tracker


class CAPMTracker(Tracker):
class AlphaBetaTracker(Tracker):
"""Tracks the Alpha and Beta"""

def __init__(self, price_type="DEFAULT"):
self.mkt_returns = []
self.acc_returns = []
Expand All @@ -27,7 +28,7 @@ def _get_market_returns(self, prices: dict[str, float]):
result += prices[symbol] / self.last_prices[symbol] - 1.0
return result / cnt

def log(self, event: Event, account, signals, orders):
def trace(self, event: Event, account, signals, orders):
prices = {item.symbol: item.price(self.price_type) for item in event.price_items.values()}
equity = account.equity
if self.init:
Expand All @@ -41,13 +42,6 @@ def log(self, event: Event, account, signals, orders):
self.last_equity = equity
self.init = True

def __repr__(self) -> str:
alpha, beta = self.alpha_beta()
table = PrettyTable(["Metric", "Value"], float_format=".2", align="r")
table.add_row(["alpha %", alpha * 100])
table.add_row(["beta", beta])
return table.get_string()

def alpha_beta(self, risk_free_return=0.0) -> Tuple[float, float]:
if not self.start_time or not self.end_time:
return float("nan"), float("nan")
Expand Down
64 changes: 19 additions & 45 deletions roboquant/trackers/basictracker.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,44 @@
from dataclasses import dataclass
from datetime import datetime
import logging
from .tracker import Tracker

from roboquant.trackers.tracker import Tracker
from roboquant.account import Account
from roboquant.event import Event
from roboquant.order import Order
from roboquant.signal import Signal
from prettytable import PrettyTable

logger = logging.getLogger(__name__)


@dataclass
class BasicTracker(Tracker):
"""Tracks a number of basic metrics:
- start- and end-time
- total number of events, items, signals and orders
- equity
- last time
- total number of events, items, signals and orders until that time
This tracker adds little overhead to a run, both CPU and memory wise.
"""

def __init__(self, price_type="DEFAULT"):
self.start_time = None
self.end_time = None
time: datetime | None
items: int
orders: int
signals: int
events: int

def __init__(self, output=False):
self.time = None
self.items = 0
self.orders = 0
self.signals = 0
self.events = 0
self.equity = None
self.buying_power = 0.0

def log(self, event: Event, account: Account, signals: dict[str, Signal], orders: list[Order]):

if self.start_time is None:
self.start_time = event.time
self.__output = output

self.end_time = event.time
def trace(self, event: Event, account: Account, signals: dict[str, Signal], orders: list[Order]):
self.time = event.time
self.items += len(event.items)
self.orders += len(orders)
self.events += 1
self.signals += len(signals)
self.equity = account.equity
self.buying_power = account.buying_power

if logger.isEnabledFor(logging.INFO):
logger.info(
"time=%s events=%s items=%s signals=%s orders=%s equity=%s, buying-power=%s",
self.end_time,
self.events,
self.items,
self.signals,
self.orders,
self.equity,
self.buying_power
)

def __repr__(self) -> str:

def to_timefmt(time: datetime | None):
return "-" if time is None else time.strftime("%Y-%m-%d %H:%M:%S")

p = PrettyTable(["metric", "value"], align="r", float_format=".2")

p.add_row(["start", to_timefmt(self.start_time)])
p.add_row(["end", to_timefmt(self.end_time)])
p.add_row(["events", self.events])
p.add_row(["items", self.items])
p.add_row(["signals", self.signals])
p.add_row(["orders", self.orders])
return p.get_string()
if self.__output:
print(self.__repr__() + "\n")
55 changes: 46 additions & 9 deletions roboquant/trackers/equitytracker.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,56 @@
from datetime import datetime
from .tracker import Tracker
from roboquant.timeframe import Timeframe
from roboquant.trackers.tracker import Tracker


class EquityTracker(Tracker):
"""Tracks the time of an event and the equity at that moment.
If multiple events happen at the same time, only the first one will be registered.
If multiple events happen at the same time, only the equity for first one will be registered.
"""

def __init__(self):
self.timeline = []
self.equity = []
self.last = datetime.fromisoformat("1900-01-01T00:00:00+00:00")
self.equities = []
self.__last = None

def log(self, event, account, signals, orders):
if event.time > self.last:
def trace(self, event, account, signals, orders):
if self.__last is None or event.time > self.__last:
self.timeline.append(event.time)
self.equity.append(account.equity)
self.last = event.time
self.equities.append(account.equity)
self.__last = event.time

def timeframe(self):
return Timeframe(self.timeline[0], self.timeline[-1], True)

def pnl(self, annualized=False):
"""Return the profit & loss percentage, optionally annualized from the recorded durtion"""
pnl = self.equities[-1]/self.equities[0] - 1
if annualized:
return self.timeframe().annualize(pnl)
else:
return pnl

def max_drawdown(self):
max_equity = self.equities[0]
result = 0.0
for equity in self.equities:
if equity > max_equity:
max_equity = equity

dd = (equity - max_equity) / max_equity
if dd < result:
result = dd

return result

def max_gain(self):
min_equity = self.equities[0]
result = 0.0
for equity in self.equities:
if equity < min_equity:
min_equity = equity

gain = (equity - min_equity) / min_equity
if gain > result:
result = gain

return result
Loading

0 comments on commit bc60038

Please sign in to comment.