Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[run]
omit =
pytrade/events/*
pytrade/interfaces/*
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.testing.unittestArgs": [
"python.defaultInterpreterPath": ".venv/bin/python",
"python.testing.pytestArgs": [
"-v",
"tests/test_*.py"
],
Expand Down
9 changes: 7 additions & 2 deletions makefiles/local.mk
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,20 @@ mypy-local:
lint-local: ##@lint Run lint tools
lint-local: bandit-local black-local flake8-local isort-local mypy-local

.PHONY: clean-imports
clean-imports: ##@local Remove unused imports
clean-imports:
autoflake --in-place --remove-all-unused-imports --recursive pytrade tests

.PHONY: reformat
reformat: ##@local Reformat module
reformat: files ?= ${SERVICE} tests
reformat:
reformat: clean-imports
${POETRY} run isort --overwrite-in-place ${files}
${POETRY} run black ${files}

PHONY: test-local
test-local: ##@local Run test suite
test-local: venv
${POETRY} run pytest -s --tb=native --durations=5 --cov=${SERVICE} --cov-report=html tests
${POETRY} run pytest -s --tb=native --durations=5 --cov=${SERVICE} --cov-report=term-missing tests
${POETRY} run coverage report --fail-under=90
1,044 changes: 563 additions & 481 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pandas = "^2.2.2"
pandas-stubs = "^2.2.2.240603"
backtesting = "^0.3.3"
multimethod = "^1.12"
autoflake = "^2.3.1"


[tool.poetry.group.dev.dependencies]
Expand Down Expand Up @@ -53,6 +54,6 @@ exclude_also = [
]
omit = [
"pytrade/interfaces/*",
"pytrade/models/*"
"pytrade/events/*"
]

32 changes: 12 additions & 20 deletions pytrade/broker.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,34 @@
from typing import Callable, List
from typing import List

from pytrade.instruments import Granularity, Instrument
from pytrade.interfaces.broker import IBroker
from pytrade.interfaces.client import IClient
from pytrade.models.instruments import Candlestick, FxInstrument, Granularity
from pytrade.models.order import OrderRequest
from pytrade.models import Order


class FxBroker(IBroker):
class Broker(IBroker):

def __init__(self, client: IClient):
self.client = client
self._pending_orders: List[OrderRequest] = []
self._orders: List[Order] = []

@property
def equity(self) -> float:
raise NotImplementedError
# return self._cash + sum(trade.pl for trade in self.trades)

@property
def margin_available(self) -> float:
raise NotImplementedError
# From https://github.com/QuantConnect/Lean/pull/3768
# margin_used = sum(trade.value / self._leverage for trade in self.trades)
# return max(0, self.equity - margin_used)

def order(self, order: OrderRequest):
self._pending_orders.append(order)
def order(self, order: Order):
self._orders.append(order)

def process_orders(self):
for order in self._pending_orders:
for order in self._orders:
self.client.order(order)

self._pending_orders.clear()
self._orders.clear()

def subscribe(
self,
instrument: FxInstrument,
granularity: Granularity,
callback: Callable[[Candlestick], None],
):
self.client.subscribe(instrument, granularity, callback)
def subscribe(self, instrument: Instrument, granularity: Granularity):
# self.client.subscribe(instrument, granularity)
pass
160 changes: 19 additions & 141 deletions pytrade/models/instruments.py → pytrade/data.py
Original file line number Diff line number Diff line change
@@ -1,146 +1,12 @@
from datetime import datetime, timedelta
from enum import Enum
from datetime import timedelta
from typing import Optional

import pandas as pd
import pytz
from pandas import Timestamp

from pytrade.events.event import Event
from pytrade.interfaces.data import IInstrumentData


class Granularity(Enum):
M1 = "M1"
M5 = "M5"
M15 = "M15"
H1 = "H1"
H4 = "H4"
D1 = "D1"


MINUTES_MAP = {
Granularity.M1: 1,
Granularity.M5: 5,
Granularity.M15: 15,
Granularity.H1: 60,
Granularity.H4: 240,
Granularity.D1: 1440,
}


class FxInstrument(Enum):
AUDJPY = "AUD/JPY"
AUDNZD = "AUD/NZD"
AUDUSD = "AUD/USD"
CADJPY = "CAD/JPY"
CHFJPY = "CHF/JPY"
EURCHF = "EUR/CHF"
EURGBP = "EUR/GBP"
EURJPY = "EUR/JPY"
EURPLN = "EUR/PLN"
EURUSD = "EUR/USD"
GBPJPY = "GBP/JPY"
GBPUSD = "GBP/USD"
NZDUSD = "NZD/USD"
USDCAD = "USD/CAD"
USDCHF = "USD/CHF"
USDJPY = "USD/JPY"
USDMXN = "USD/MXN"
USDRUB = "USD/RUB"
USDTRY = "USD/TRY"
USDZAR = "USD/ZAR"


instrument_lookup = {m.value: m for m in FxInstrument}


class CandleSubscription:

def __init__(self, instrument: FxInstrument, granularity: Granularity):
self._instrument = instrument
self._granularity = granularity

@property
def granularity(self) -> Granularity:
return self._granularity

@property
def instrument(self) -> FxInstrument:
return self._instrument

def __hash__(self):
return hash((self.instrument, self.granularity))

def __gt__(self, other):
return (
isinstance(other, CandleSubscription)
and self.instrument.value < other.instrument.value
and self.granularity.value > other.granularity.value
)

def __lt__(self, other):
return (
isinstance(other, CandleSubscription)
and self.instrument.value > other.instrument.value
and self.granularity.value < other.granularity.value
)

def __eq__(self, other):
return (
isinstance(other, CandleSubscription)
and self.instrument.value == other.instrument.value
and self.granularity.value == other.granularity.value
)


class Candlestick:

def __init__(
self,
instrument: FxInstrument,
granularity: Granularity,
open: float,
high: float,
low: float,
close: float,
timestamp: Timestamp,
):
self.instrument = instrument
self.granularity = granularity
self.open = open
self.high = high
self.low = low
self.close = close
self.timestamp = timestamp

def to_dict(self):
return {
"Timestamp": self.timestamp,
"Instrument": self.instrument,
"Open": self.open,
"High": self.open,
"Low": self.open,
"Close": self.open,
}


class TickData:

instrument: FxInstrument
timestamp: datetime
bid: float
ask: float

def __init__(self, instrument: str, timestamp: str, bid: str, ask: str):
self.instrument = instrument_lookup[instrument]
tz = pytz.timezone("UTC")
self.timestamp = datetime.strptime(timestamp, "%Y%m%d %H:%M:%S.%f").astimezone(
tz
)
self.bid = float(bid)
self.ask = float(ask)

from pytrade.instruments import Candlestick, Granularity, Instrument
from pytrade.interfaces.data import IDataContext, IInstrumentData

COLUMNS = ["Timestamp", "Instrument", "Open", "High", "Low", "Close"]

Expand All @@ -163,14 +29,26 @@ def __init__(
"Dataframe does not have a datetime index and does not have a 'Timestamp' column"
)
self._max_size: Optional[int] = max_size
self.__instrument: Optional[FxInstrument] = None
self.__instrument: Optional[Instrument] = None
self.__granularity: Optional[Granularity] = None
self.__update_event = Event()

@property
def df(self):
return self._data

@property
def instrument(self):
return self.__instrument

@property
def granularity(self):
return self.__granularity

@property
def timestamp(self) -> Timestamp:
return self._data.index[-1]

@property
def on_update(self):
return self.__update_event
Expand Down Expand Up @@ -215,10 +93,10 @@ def update(self, candlestick: Candlestick):
self.__update_event()


class CandleData:
class CandleData(IDataContext):

def __init__(self, max_size=1000):
self._data: dict[tuple[FxInstrument, Granularity], InstrumentCandles] = {}
self._data: dict[tuple[Instrument, Granularity], InstrumentCandles] = {}
self._max_size = max_size

def __new__(cls, *args, **kwargs):
Expand All @@ -227,7 +105,7 @@ def __new__(cls, *args, **kwargs):
# Need to handle case where instantiatied and different max size is provided
return cls.instance

def get(self, instrument: FxInstrument, granularity: Granularity):
def get(self, instrument: Instrument, granularity: Granularity):
key = (instrument, granularity)
instrument_candles: InstrumentCandles = self._data.get(
(instrument, granularity), InstrumentCandles(max_size=self._max_size)
Expand Down
File renamed without changes.
6 changes: 0 additions & 6 deletions pytrade/events/candlestick_event.py

This file was deleted.

2 changes: 1 addition & 1 deletion pytrade/events/event.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Callable


class Event:
class Event: # pragma: no cover

def __init__(self):
self.__callbacks: list[Callable[[], None]] = []
Expand Down
61 changes: 61 additions & 0 deletions pytrade/indicator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from abc import abstractmethod

import numpy as np

from pytrade.interfaces.data import IInstrumentData


class Indicator:

def __init__(self, data: IInstrumentData):
self._data = data
data.on_update += self._update
self._values = self._run()

def _update(self):
self._values = self._run()

@abstractmethod
def _run(self) -> np.ndarray:
raise NotImplementedError()

@property
def value(self):
return self._values.iloc[-1] if len(self._values) > 0 else None

@property
def to_array(self):
return self._values

def __eq__(self, other):
result = False
if isinstance(other, Indicator):
result = self.value == other.value
else:
result = self.value == other

return result

def __gt__(self, other):
result = False
if isinstance(other, Indicator):
result = self.value > other.value
else:
result = self.value > other

return result

def __lt__(self, other):
result = False
if isinstance(other, Indicator):
result = self.value < other.value
else:
result = self.value < other

return result

def __bool__(self):
return bool(self.value)

def __float__(self):
return float(self.value)
Loading
Loading