mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-23 16:58:07 +00:00
Merge branch 'dev'
This commit is contained in:
+84
-56
@@ -1,12 +1,12 @@
|
|||||||
"""Candle and Candles classes for handling bars from the MetaTrader 5 terminal."""
|
"""Candle and Candles classes for handling bars from the MetaTrader 5 terminal."""
|
||||||
|
|
||||||
import time
|
from datetime import datetime
|
||||||
from typing import Type, Self, Iterable
|
from typing import Type, Self, Iterable
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from pandas import DataFrame, Series
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pandas_ta as ta
|
import pandas_ta as ta
|
||||||
|
from pandas import DataFrame, Series, DatetimeIndex, Timestamp
|
||||||
|
|
||||||
from ..core.constants import TimeFrame
|
from ..core.constants import TimeFrame
|
||||||
|
|
||||||
@@ -18,17 +18,18 @@ class Candle:
|
|||||||
Candlesticks. You can subclass this class for added customization.
|
Candlesticks. You can subclass this class for added customization.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
time (int): Period start time.
|
time (float): Period start time.
|
||||||
open (int): Open price
|
open (float): Open price
|
||||||
high (float): The highest price of the period
|
high (float): The highest price of the period
|
||||||
low (float): The lowest price of the period
|
low (float): The lowest price of the period
|
||||||
close (float): Close price
|
close (float): Close price
|
||||||
tick_volume (float): Tick volume
|
tick_volume (float): Tick volume
|
||||||
real_volume (float): Trade volume
|
real_volume (float): Trade volume
|
||||||
spread (float): Spread
|
spread (float): Spread
|
||||||
Index (int): Custom attribute representing the position of the candle in a sequence.
|
index (Timestamp): Index of the object in the DataFrame, a timestamp
|
||||||
|
Index (int): Custom attribute representing the position of the candle for integer-location based indexing
|
||||||
"""
|
"""
|
||||||
time: int
|
time: float
|
||||||
open: float
|
open: float
|
||||||
high: float
|
high: float
|
||||||
low: float
|
low: float
|
||||||
@@ -36,6 +37,7 @@ class Candle:
|
|||||||
real_volume: float
|
real_volume: float
|
||||||
spread: float
|
spread: float
|
||||||
tick_volume: float
|
tick_volume: float
|
||||||
|
index: Timestamp
|
||||||
Index: int
|
Index: int
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -47,8 +49,9 @@ class Candle:
|
|||||||
"""
|
"""
|
||||||
if not all(i in kwargs for i in ["open", "high", "low", "close"]):
|
if not all(i in kwargs for i in ["open", "high", "low", "close"]):
|
||||||
raise ValueError("Candle must be instantiated with open, high, low and close prices")
|
raise ValueError("Candle must be instantiated with open, high, low and close prices")
|
||||||
self.time = kwargs.pop("time", int(time.time()))
|
self.time = kwargs.pop("time", Timestamp.now().timestamp())
|
||||||
self.Index = kwargs.pop("Index", 0)
|
self.Index = kwargs.pop("Index", 0)
|
||||||
|
self.index = kwargs.pop("index", Timestamp(self.time, unit="s", tz=datetime.now().astimezone().tzinfo))
|
||||||
self.real_volume = kwargs.pop("real_volume", 0)
|
self.real_volume = kwargs.pop("real_volume", 0)
|
||||||
self.spread = kwargs.pop("spread", 0)
|
self.spread = kwargs.pop("spread", 0)
|
||||||
self.tick_volume = kwargs.pop("tick_volume", 0)
|
self.tick_volume = kwargs.pop("tick_volume", 0)
|
||||||
@@ -56,7 +59,7 @@ class Candle:
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
"%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)"
|
"%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s, index=%(index)s)"
|
||||||
% {
|
% {
|
||||||
"class": self.__class__.__name__,
|
"class": self.__class__.__name__,
|
||||||
"open": self.open,
|
"open": self.open,
|
||||||
@@ -64,6 +67,7 @@ class Candle:
|
|||||||
"low": self.low,
|
"low": self.low,
|
||||||
"close": self.close,
|
"close": self.close,
|
||||||
"time": self.time,
|
"time": self.time,
|
||||||
|
"index": self.index.isoformat(),
|
||||||
"Index": self.Index,
|
"Index": self.Index,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -131,13 +135,18 @@ class Candle:
|
|||||||
keys = include or set(self.__dict__.keys()).difference(exclude)
|
keys = include or set(self.__dict__.keys()).difference(exclude)
|
||||||
return {k: v for k, v in self if k in keys}
|
return {k: v for k, v in self if k in keys}
|
||||||
|
|
||||||
|
def to_series(self) -> Series:
|
||||||
|
"""Returns a Series Object"""
|
||||||
|
return Series(self.dict(exclude={"Index", "index"}))
|
||||||
|
|
||||||
|
|
||||||
class Candles:
|
class Candles:
|
||||||
"""An iterable container class of Candle objects in chronological order.
|
"""An iterable container class of Candle objects in chronological order.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
Index (Series['int']): A pandas Series of the indexes of all candles in the object.
|
index (DatetimeIndex): DatetimeIndex of the DataFrame object.
|
||||||
time (Series['int']): A pandas Series of the time of all candles in the object.
|
Index (Series['int']): A pandas Series of the indexes of all candles in the object:
|
||||||
|
time (Series['float']): A pandas Series of the time of all candles in the object.
|
||||||
open (Series[float]): A pandas Series of the opening price of all candles in the object.
|
open (Series[float]): A pandas Series of the opening price of all candles in the object.
|
||||||
high (Series[float]): A pandas Series of the high price of all candles in the object.
|
high (Series[float]): A pandas Series of the high price of all candles in the object.
|
||||||
low (Series[float]): A pandas Series of the low price of all candles in the object.
|
low (Series[float]): A pandas Series of the low price of all candles in the object.
|
||||||
@@ -155,7 +164,7 @@ class Candles:
|
|||||||
The candle class can be customized by subclassing the Candle class and passing the subclass as the candle
|
The candle class can be customized by subclassing the Candle class and passing the subclass as the candle
|
||||||
keyword argument, or defining it on the class body as a class attribute.
|
keyword argument, or defining it on the class body as a class attribute.
|
||||||
"""
|
"""
|
||||||
|
index: DatetimeIndex
|
||||||
Index: Series
|
Index: Series
|
||||||
time: Series
|
time: Series
|
||||||
open: Series
|
open: Series
|
||||||
@@ -189,18 +198,20 @@ class Candles:
|
|||||||
raise ValueError(f"Cannot create DataFrame from object of {type(data)}")
|
raise ValueError(f"Cannot create DataFrame from object of {type(data)}")
|
||||||
|
|
||||||
self._data = data.loc[::-1] if flip else data
|
self._data = data.loc[::-1] if flip else data
|
||||||
if 'time' in self._data.columns and self._data.index.name != 'time':
|
if 'time' in self._data.columns:
|
||||||
self._data.set_index('time', inplace=True, drop=False)
|
dtype = pd.DatetimeTZDtype(unit='s', tz=datetime.now().astimezone().tzinfo)
|
||||||
|
self._data.index = pd.DatetimeIndex(self._data.time, dtype=dtype)
|
||||||
|
|
||||||
self.Candle = candle_class or Candle
|
self.Candle = candle_class or Candle
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self._data.__repr__()
|
return repr(self._data)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self._data.index)
|
return len(self._data.index)
|
||||||
|
|
||||||
def __contains__(self, item: Candle):
|
def __contains__(self, item: Candle):
|
||||||
return item.time == self._data.loc[int(item.time)].time
|
return item.time == self[item.Index].time
|
||||||
|
|
||||||
def __getitem__(self, index: slice | int | str) -> Self | Series | Candle:
|
def __getitem__(self, index: slice | int | str) -> Self | Series | Candle:
|
||||||
if isinstance(index, slice):
|
if isinstance(index, slice):
|
||||||
@@ -208,15 +219,18 @@ class Candles:
|
|||||||
data = self._data.iloc[index]
|
data = self._data.iloc[index]
|
||||||
return cls(data=data)
|
return cls(data=data)
|
||||||
|
|
||||||
elif isinstance(index, str):
|
if isinstance(index, str):
|
||||||
|
if index == "index":
|
||||||
|
return self._data.index
|
||||||
if index == "Index":
|
if index == "Index":
|
||||||
return Series(self._data.index)
|
return Series(range(len(self._data)))
|
||||||
return self._data[index]
|
return self._data[index]
|
||||||
|
|
||||||
elif isinstance(index, int):
|
if isinstance(index, int):
|
||||||
candle = self._data.iloc[index]
|
candle = self._data.iloc[index]
|
||||||
_index = index if index >= 0 else len(self) + index
|
Index = index if index >= 0 else len(self) + index
|
||||||
return self.Candle(**candle, Index=_index)
|
_index = self._data.index[index]
|
||||||
|
return self.Candle(**candle, Index=Index, index=_index)
|
||||||
raise TypeError(f"Expected int, slice or str got {type(index)}")
|
raise TypeError(f"Expected int, slice or str got {type(index)}")
|
||||||
|
|
||||||
def __setitem__(self, index, value: Series):
|
def __setitem__(self, index, value: Series):
|
||||||
@@ -229,13 +243,27 @@ class Candles:
|
|||||||
if item in self._data.columns:
|
if item in self._data.columns:
|
||||||
return self._data[item]
|
return self._data[item]
|
||||||
|
|
||||||
|
if item == "index":
|
||||||
|
return self._data.index
|
||||||
|
|
||||||
if item == "Index":
|
if item == "Index":
|
||||||
return Series(self._data.index)
|
return Series(range(len(self._data)))
|
||||||
raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}")
|
raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}")
|
||||||
|
|
||||||
|
def __reversed__(self):
|
||||||
|
for index, row in enumerate(iter(self._data[::-1].iloc)):
|
||||||
|
row = row.to_dict()
|
||||||
|
index = len(self._data) - index - 1
|
||||||
|
row["Index"] = index
|
||||||
|
row["index"] = self._data.index[index]
|
||||||
|
yield self.Candle(**row)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return (self.Candle(**row.to_dict(), Index=ind) for ind, row in enumerate(self._data.iloc))
|
for index, row in enumerate(iter(self._data.iloc)):
|
||||||
# return (self.Candle(**row._asdict()) for row in self._data.itertuples())
|
row = row.to_dict()
|
||||||
|
row["Index"] = index
|
||||||
|
row["index"] = self._data.index[index]
|
||||||
|
yield self.Candle(**row)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timeframe(self):
|
def timeframe(self):
|
||||||
@@ -282,39 +310,39 @@ class Candles:
|
|||||||
res = self._data.rename(columns=kwargs, inplace=inplace)
|
res = self._data.rename(columns=kwargs, inplace=inplace)
|
||||||
return self if inplace else self.__class__(data=res)
|
return self if inplace else self.__class__(data=res)
|
||||||
|
|
||||||
def __iadd__(self, row: DataFrame | Series):
|
def __iadd__(self, other: Self) -> Self:
|
||||||
"""Add a new row to the candles class."""
|
"""Perform in place addition of candles"""
|
||||||
# self._data = pd.concat([self._data, row])
|
data_copy = self._data.copy()
|
||||||
if isinstance(row, Series):
|
other = other._data
|
||||||
self._data.loc[int(row.time)] = row
|
for index, row in zip(other.index, iter(other.iloc)):
|
||||||
|
data_copy.loc[index] = row
|
||||||
elif isinstance(row, DataFrame):
|
self._data = data_copy.sort_index()
|
||||||
for r in row.iloc:
|
|
||||||
self._data.loc[int(r.time)] = r
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __add__(self, row: DataFrame | Series):
|
def __add__(self, other: Self) -> Self:
|
||||||
"""Add a new row to the candles class."""
|
"""Add two candles object and return a new one"""
|
||||||
if isinstance(row, Series):
|
data = self._data.copy()
|
||||||
data = self. pd.concat([self._data, pd.DataFrame(row).T])
|
for index, row in zip(other._data.index, iter(other._data.iloc)):
|
||||||
return self.__class__(data=data)
|
data.loc[index] = row
|
||||||
|
return self.__class__(data=data.sort_index())
|
||||||
|
|
||||||
elif isinstance(row, DataFrame):
|
def add(self, obj: DataFrame | Series | Candle) -> Self:
|
||||||
data = pd.concat([self._data, row])
|
"""Add new row(s) to the candles class."""
|
||||||
return self.__class__(data=data)
|
if isinstance(obj, Series):
|
||||||
|
index = Timestamp(obj.time, unit="s", tz=datetime.now().astimezone().tzinfo)
|
||||||
def add(self, row: DataFrame | Series) -> bool:
|
self._data.loc[index] = obj
|
||||||
"""Add a new row to the candles class."""
|
self._data = self._data.sort_index()
|
||||||
new = True
|
return self
|
||||||
if isinstance(row, Series):
|
elif isinstance(obj, DataFrame):
|
||||||
if (index := int(row.time)) in self._data.index:
|
data = self._data.copy()
|
||||||
new = False
|
for index, row in zip(obj.index, iter(obj.iloc)):
|
||||||
self._data.loc[index] = row
|
index = index if isinstance(index, Timestamp) else Timestamp(row.time, unit="s", tz=datetime.now().astimezone().tzinfo)
|
||||||
|
data.loc[index] = row
|
||||||
elif isinstance(row, DataFrame):
|
self._data = data.sort_index()
|
||||||
for r in row.iloc:
|
return self
|
||||||
if (index := int(r.time)) in self._data.index:
|
elif isinstance(obj, Candle):
|
||||||
new = False
|
self._data.loc[obj.index] = obj.to_series()
|
||||||
self._data.loc[index] = r
|
self._data = self._data.sort_index()
|
||||||
return new
|
return self
|
||||||
|
else:
|
||||||
|
raise TypeError("Expected Series, DataFrame or Candle, got {}".format(type(obj)))
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
if tick is not None:
|
if tick is not None:
|
||||||
tick = Tick(**tick._asdict())
|
tick = Tick(**tick._asdict())
|
||||||
setattr(self, "tick", tick) if not name else ...
|
setattr(self, "tick", tick) if not name else ...
|
||||||
return tick
|
return tick
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.warning("%s: Unable to get tick for %s", err, self.name)
|
logger.warning("%s: Unable to get tick for %s", err, self.name)
|
||||||
return None
|
return None
|
||||||
@@ -92,7 +92,7 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
info = await self.mt5.symbol_info(self.name)
|
info = await self.mt5.symbol_info(self.name)
|
||||||
if info is not None:
|
if info is not None:
|
||||||
info = info._asdict()
|
info = info._asdict()
|
||||||
# self.set_attributes(**info)
|
self.set_attributes(**info)
|
||||||
return SymbolInfo(**info)
|
return SymbolInfo(**info)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
"""Initialize the symbol by pulling properties from the terminal
|
"""Initialize the symbol by pulling properties from the terminal
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: Returns True if symbol info was successful initialized
|
bool: Returns True if symbol info was successfully initialized
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
select = await self.mt5.symbol_select(self.name, True)
|
select = await self.mt5.symbol_select(self.name, True)
|
||||||
@@ -229,7 +229,7 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
"""
|
"""
|
||||||
return self.volume_min
|
return self.volume_min
|
||||||
|
|
||||||
async def convert_currency(self, *, amount: float, from_currency: str, to_currency: str) -> float:
|
async def convert_currency(self, *, amount: float, from_currency: str, to_currency: str) -> float | None:
|
||||||
"""Convert a given amount from one currency to the other.
|
"""Convert a given amount from one currency to the other.
|
||||||
Args:
|
Args:
|
||||||
amount: Amount to convert
|
amount: Amount to convert
|
||||||
@@ -245,10 +245,10 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
|
|
||||||
pair = f"{base}{quote}"
|
pair = f"{base}{quote}"
|
||||||
tick = await self.info_tick(name=pair)
|
tick = await self.info_tick(name=pair)
|
||||||
if tick is not None:
|
return round(amount / tick.ask, 2)
|
||||||
return round(amount / tick.ask, 2)
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.warning(f"{err}: Currency conversion failed: Unable to convert {amount} in {quote} to {base}")
|
logger.warning(f"{err}: Currency conversion failed: Unable to convert {amount} in {quote} to {base}")
|
||||||
|
return None
|
||||||
|
|
||||||
@backoff_decorator
|
@backoff_decorator
|
||||||
async def copy_rates_from(self, *, timeframe: TimeFrame, date_from: datetime | int, count: int = 500) -> Candles:
|
async def copy_rates_from(self, *, timeframe: TimeFrame, date_from: datetime | int, count: int = 500) -> Candles:
|
||||||
|
|||||||
+85
-14
@@ -1,8 +1,9 @@
|
|||||||
"""Module for working with price ticks."""
|
"""Module for working with price ticks."""
|
||||||
|
|
||||||
from typing import Iterable, Self
|
from typing import Iterable, Self
|
||||||
import time
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
from pandas import DataFrame, Series
|
from pandas import DataFrame, Series
|
||||||
import pandas_ta as ta
|
import pandas_ta as ta
|
||||||
|
|
||||||
@@ -23,7 +24,6 @@ class Tick:
|
|||||||
volume_real (float): Volume for the current Last price
|
volume_real (float): Volume for the current Last price
|
||||||
Index (int): Custom attribute representing the position of the tick in a sequence.
|
Index (int): Custom attribute representing the position of the tick in a sequence.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
time: float
|
time: float
|
||||||
bid: float
|
bid: float
|
||||||
ask: float
|
ask: float
|
||||||
@@ -32,6 +32,7 @@ class Tick:
|
|||||||
time_msc: float
|
time_msc: float
|
||||||
flags: TickFlag
|
flags: TickFlag
|
||||||
volume_real: float
|
volume_real: float
|
||||||
|
index: int | float
|
||||||
Index: int
|
Index: int
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -39,14 +40,15 @@ class Tick:
|
|||||||
present"""
|
present"""
|
||||||
if not all(key in kwargs for key in ["bid", "ask", "last", "volume"]):
|
if not all(key in kwargs for key in ["bid", "ask", "last", "volume"]):
|
||||||
raise ValueError("bid, ask, last and volume, time must be present in the keyword arguments")
|
raise ValueError("bid, ask, last and volume, time must be present in the keyword arguments")
|
||||||
|
self.time = kwargs.pop("time", datetime.now().timestamp())
|
||||||
|
self.time_msc = kwargs.pop("time_msc", self.time * 1000)
|
||||||
self.Index = kwargs.pop("Index", 0)
|
self.Index = kwargs.pop("Index", 0)
|
||||||
self.time = kwargs.pop("time", time.monotonic())
|
self.index = kwargs.pop("index", self.time_msc)
|
||||||
self.time_msc = int(self.time * 1000)
|
|
||||||
self.set_attributes(**kwargs)
|
self.set_attributes(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
"%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)"
|
"%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s, index=%(index)s)"
|
||||||
% {
|
% {
|
||||||
"class": self.__class__.__name__,
|
"class": self.__class__.__name__,
|
||||||
"time": self.time,
|
"time": self.time,
|
||||||
@@ -54,18 +56,19 @@ class Tick:
|
|||||||
"ask": self.ask,
|
"ask": self.ask,
|
||||||
"last": self.last,
|
"last": self.last,
|
||||||
"volume": self.volume,
|
"volume": self.volume,
|
||||||
|
"index": self.index,
|
||||||
"Index": self.Index,
|
"Index": self.Index,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def __eq__(self, other: Self):
|
def __eq__(self, other: Self):
|
||||||
return self.time == other.time
|
return self.time_msc == other.time_msc
|
||||||
|
|
||||||
def __lt__(self, other: Self):
|
def __lt__(self, other: Self):
|
||||||
return self.time < other.time
|
return self.time_msc < other.time_msc
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.time)
|
return hash(self.time_msc)
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
return self.__dict__[item]
|
return self.__dict__[item]
|
||||||
@@ -102,10 +105,12 @@ class Tick:
|
|||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def to_series(self) -> pd.Series:
|
||||||
|
"""Returns a Series Object"""
|
||||||
|
return Series(self.dict(exclude={"Index", "index"}))
|
||||||
|
|
||||||
class Ticks:
|
class Ticks:
|
||||||
"""Container class for price ticks. Arrange in chronological order. Supports iteration, slicing and assignment"""
|
"""Container class for price ticks. Arrange in chronological order. Supports iteration, slicing and assignment"""
|
||||||
|
|
||||||
time: Series
|
time: Series
|
||||||
bid: Series
|
bid: Series
|
||||||
ask: Series
|
ask: Series
|
||||||
@@ -115,6 +120,7 @@ class Ticks:
|
|||||||
flags: Series
|
flags: Series
|
||||||
volume_real: Series
|
volume_real: Series
|
||||||
Index: Series
|
Index: Series
|
||||||
|
index: Series
|
||||||
|
|
||||||
def __init__(self, *, data: DataFrame | Iterable | Self, flip=False):
|
def __init__(self, *, data: DataFrame | Iterable | Self, flip=False):
|
||||||
"""Initialize the Ticks class. Creates a DataFrame of price ticks from the data argument.
|
"""Initialize the Ticks class. Creates a DataFrame of price ticks from the data argument.
|
||||||
@@ -134,8 +140,11 @@ class Ticks:
|
|||||||
raise ValueError(f"Cannot create DataFrame from object of {type(data)}")
|
raise ValueError(f"Cannot create DataFrame from object of {type(data)}")
|
||||||
self._data = data.iloc[::-1] if flip else data
|
self._data = data.iloc[::-1] if flip else data
|
||||||
|
|
||||||
|
if 'time_msc' in self._data.columns:
|
||||||
|
self._data.index = self._data.time_msc
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self._data.__repr__()
|
return repr(self._data)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return self._data.shape[0]
|
return self._data.shape[0]
|
||||||
@@ -146,20 +155,34 @@ class Ticks:
|
|||||||
def __getattr__(self, item):
|
def __getattr__(self, item):
|
||||||
if item in list(self._data.columns.values):
|
if item in list(self._data.columns.values):
|
||||||
return self._data[item]
|
return self._data[item]
|
||||||
|
|
||||||
|
if item == "index":
|
||||||
|
return self._data.index
|
||||||
|
|
||||||
|
if item == "Index":
|
||||||
|
return Series(range(len(self._data)))
|
||||||
raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}")
|
raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}")
|
||||||
|
|
||||||
def __getitem__(self, index) -> Tick | Self:
|
def __getitem__(self, index) -> Tick | Self:
|
||||||
if isinstance(index, slice):
|
if isinstance(index, slice):
|
||||||
cls = self.__class__
|
cls = self.__class__
|
||||||
data = self._data.iloc[index]
|
data = self._data.iloc[index]
|
||||||
data.reset_index(drop=True, inplace=True)
|
|
||||||
return cls(data=data)
|
return cls(data=data)
|
||||||
|
|
||||||
if isinstance(index, str):
|
if isinstance(index, str):
|
||||||
|
if index == "index":
|
||||||
|
return self._data.index
|
||||||
|
if index == "Index":
|
||||||
|
return Series(range(len(self._data)))
|
||||||
return self._data[index]
|
return self._data[index]
|
||||||
|
|
||||||
item = self._data.iloc[index]
|
if isinstance(index, int):
|
||||||
return Tick(**item, Index=index)
|
tick = self._data.iloc[index]
|
||||||
|
Index = index if index >= 0 else len(self) + index
|
||||||
|
_index = self._data.index[index]
|
||||||
|
return Tick(**tick, Index=Index, index=_index)
|
||||||
|
|
||||||
|
raise TypeError(f"Expected int, slice or str got {type(index)}")
|
||||||
|
|
||||||
def __setitem__(self, index, value: Series):
|
def __setitem__(self, index, value: Series):
|
||||||
if isinstance(value, Series):
|
if isinstance(value, Series):
|
||||||
@@ -167,8 +190,20 @@ class Ticks:
|
|||||||
return
|
return
|
||||||
raise TypeError(f"Expected Series got {type(value)}")
|
raise TypeError(f"Expected Series got {type(value)}")
|
||||||
|
|
||||||
|
def __reversed__(self):
|
||||||
|
for index, row in enumerate(iter(self._data[::-1].iloc)):
|
||||||
|
row = row.to_dict()
|
||||||
|
index = len(self._data) - index - 1
|
||||||
|
row["Index"] = index
|
||||||
|
row["index"] = self._data.index[index]
|
||||||
|
yield Tick(**row)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return (Tick(**row._asdict()) for row in self._data.itertuples())
|
for index, row in enumerate(iter(self._data.iloc)):
|
||||||
|
row = row.to_dict()
|
||||||
|
row["Index"] = index
|
||||||
|
row["index"] = self._data.index[index]
|
||||||
|
yield Tick(**row)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ta(self):
|
def ta(self):
|
||||||
@@ -206,3 +241,39 @@ class Ticks:
|
|||||||
"""
|
"""
|
||||||
res = self._data.rename(columns=kwargs, inplace=inplace)
|
res = self._data.rename(columns=kwargs, inplace=inplace)
|
||||||
return res if inplace else self.__class__(data=res)
|
return res if inplace else self.__class__(data=res)
|
||||||
|
|
||||||
|
def __iadd__(self, other: Self) -> Self:
|
||||||
|
"""Perform in place addition of candles"""
|
||||||
|
data_copy = self._data.copy()
|
||||||
|
other = other._data
|
||||||
|
for index, row in zip(other.index, iter(other.iloc)):
|
||||||
|
data_copy.loc[index] = row
|
||||||
|
self._data = data_copy.sort_index()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __add__(self, other: Self) -> Self:
|
||||||
|
"""Add two candles object and return a new one"""
|
||||||
|
data = self._data.copy()
|
||||||
|
for index, row in zip(other._data.index, iter(other._data.iloc)):
|
||||||
|
data.loc[index] = row
|
||||||
|
return self.__class__(data=data.sort_index())
|
||||||
|
|
||||||
|
def add(self, obj: DataFrame | Series | Tick) -> Self:
|
||||||
|
"""Add new row(s) to the candles class."""
|
||||||
|
if isinstance(obj, Series):
|
||||||
|
self._data.loc[obj.index] = obj
|
||||||
|
self._data = self._data.sort_index()
|
||||||
|
return self
|
||||||
|
elif isinstance(obj, DataFrame):
|
||||||
|
data = self._data.copy()
|
||||||
|
for index, row in zip(obj.index, iter(obj.iloc)):
|
||||||
|
index = index
|
||||||
|
data.loc[index] = row
|
||||||
|
self._data = data.sort_index()
|
||||||
|
return self
|
||||||
|
elif isinstance(obj, Tick):
|
||||||
|
self._data.loc[obj.index] = obj.to_series()
|
||||||
|
self._data = self._data.sort_index()
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
raise TypeError("Expected Series, DataFrame or Candle, got {}".format(type(obj)))
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytz
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from pandas import Series
|
||||||
|
|
||||||
from aiomql.lib.candle import Candle, Candles
|
from aiomql.lib.candle import Candle, Candles
|
||||||
from aiomql.core.meta_trader import MetaTrader
|
from aiomql.core.meta_trader import MetaTrader
|
||||||
from aiomql.core.constants import TimeFrame
|
from aiomql.core.constants import TimeFrame
|
||||||
@@ -11,8 +12,8 @@ from aiomql.core.constants import TimeFrame
|
|||||||
class TestCandle:
|
class TestCandle:
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_class(cls):
|
def setup_class(cls):
|
||||||
cls.bullish_candle = Candle(open=1.3421, high=1.3462, low=1.3405, close=1.3452, time=0, Index=0)
|
cls.bullish_candle = Candle(open=1.3421, high=1.3462, low=1.3405, close=1.3452)
|
||||||
cls.bearish_candle = Candle(open=1.3452, high=1.3405, low=1.3462, close=1.3421, time=1, Index=1)
|
cls.bearish_candle = Candle(open=1.3452, high=1.3405, low=1.3462, close=1.3421)
|
||||||
|
|
||||||
def test_repr(self):
|
def test_repr(self):
|
||||||
repr_str = repr(self.bearish_candle)
|
repr_str = repr(self.bearish_candle)
|
||||||
@@ -48,6 +49,10 @@ class TestCandle:
|
|||||||
assert self.bearish_candle.is_bearish()
|
assert self.bearish_candle.is_bearish()
|
||||||
assert self.bullish_candle.is_bullish()
|
assert self.bullish_candle.is_bullish()
|
||||||
|
|
||||||
|
def test_to_series(self):
|
||||||
|
ser = self.bearish_candle.to_series()
|
||||||
|
assert isinstance(ser, Series)
|
||||||
|
|
||||||
|
|
||||||
class TestCandles:
|
class TestCandles:
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
@@ -57,6 +62,13 @@ class TestCandles:
|
|||||||
rates = await mt.copy_rates_from("BTCUSD", mt.TIMEFRAME_H1, start, 200)
|
rates = await mt.copy_rates_from("BTCUSD", mt.TIMEFRAME_H1, start, 200)
|
||||||
return Candles(data=rates)
|
return Candles(data=rates)
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
async def candles_2(self):
|
||||||
|
mt = MetaTrader()
|
||||||
|
start = datetime(day=5, month=10, year=2023)
|
||||||
|
rates = await mt.copy_rates_from("BTCUSD", mt.TIMEFRAME_H1, start, 300)
|
||||||
|
return Candles(data=rates)
|
||||||
|
|
||||||
def test_get_series(self, candles):
|
def test_get_series(self, candles):
|
||||||
series = candles["open"]
|
series = candles["open"]
|
||||||
assert isinstance(series, pd.Series)
|
assert isinstance(series, pd.Series)
|
||||||
@@ -102,3 +114,35 @@ class TestCandles:
|
|||||||
assert isinstance(fas, pd.Series)
|
assert isinstance(fas, pd.Series)
|
||||||
candles["fas"] = fas
|
candles["fas"] = fas
|
||||||
assert "fas" in candles.data.columns
|
assert "fas" in candles.data.columns
|
||||||
|
|
||||||
|
def test_add_candles(self, candles, candles_2):
|
||||||
|
nc = candles + candles_2
|
||||||
|
candles += candles_2
|
||||||
|
assert len(nc) == 300
|
||||||
|
assert len(candles) == 300
|
||||||
|
|
||||||
|
def test_add_candle(self, candles):
|
||||||
|
length = len(candles)
|
||||||
|
candle = candles[-1]
|
||||||
|
now = datetime.now()
|
||||||
|
candle.time = now.timestamp()
|
||||||
|
candle.index = pd.Timestamp(candle.time, unit="s", tz=now.astimezone().tzinfo)
|
||||||
|
candles.add(candle)
|
||||||
|
assert len(candles) == length + 1
|
||||||
|
|
||||||
|
def test_add_series(self, candles):
|
||||||
|
candle = candles[-1]
|
||||||
|
length = len(candles)
|
||||||
|
now = datetime.now()
|
||||||
|
candle.time = now.timestamp()
|
||||||
|
series = candle.to_series()
|
||||||
|
candles.add(series)
|
||||||
|
assert len(candles) == length + 1
|
||||||
|
|
||||||
|
def test_add_dataframe(self, candles, candles_2):
|
||||||
|
df = candles_2._data.iloc[0:3]
|
||||||
|
now = datetime.now()
|
||||||
|
df.index = pd.DatetimeIndex(df.time, tz=now.astimezone().tzinfo)
|
||||||
|
length = len(candles)
|
||||||
|
candles.add(df)
|
||||||
|
assert len(candles) == length + len(df)
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ class TestSymbol:
|
|||||||
@pytest.fixture(scope="class", autouse=True)
|
@pytest.fixture(scope="class", autouse=True)
|
||||||
async def btc(self):
|
async def btc(self):
|
||||||
symbol = Symbol(name="BTCUSD")
|
symbol = Symbol(name="BTCUSD")
|
||||||
select = getattr(symbol, "select", False)
|
if symbol.initialized is False:
|
||||||
if select is False:
|
|
||||||
await symbol.initialize()
|
await symbol.initialize()
|
||||||
return symbol
|
return symbol
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user