diff --git a/.gitignore b/.gitignore
index 572c97b..c2ebdcd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,7 +15,6 @@ dist/
downloads/
eggs/
.eggs/
-lib/
lib64/
parts/
sdist/
@@ -45,6 +44,7 @@ nosetests.xml
coverage.xml
*,cover
.hypothesis/
+scrap.py
# Translations
*.mo
diff --git a/docs/main.md b/docs/main.md
index be020eb..53b4e4e 100644
--- a/docs/main.md
+++ b/docs/main.md
@@ -12,6 +12,8 @@
* [aiomql.bot\_builder](#aiomql.bot_builder)
* [Bot](#aiomql.bot_builder.Bot)
* [initialize](#aiomql.bot_builder.Bot.initialize)
+ * [add\_function](#aiomql.bot_builder.Bot.add_function)
+ * [add\_coroutine](#aiomql.bot_builder.Bot.add_coroutine)
* [execute](#aiomql.bot_builder.Bot.execute)
* [start](#aiomql.bot_builder.Bot.start)
* [add\_strategy](#aiomql.bot_builder.Bot.add_strategy)
@@ -107,6 +109,7 @@
* [add\_workers](#aiomql.executor.Executor.add_workers)
* [remove\_workers](#aiomql.executor.Executor.remove_workers)
* [add\_worker](#aiomql.executor.Executor.add_worker)
+ * [trade](#aiomql.executor.Executor.trade)
* [run](#aiomql.executor.Executor.run)
* [execute](#aiomql.executor.Executor.execute)
* [aiomql.history](#aiomql.history)
@@ -120,14 +123,13 @@
* [aiomql.lib.strategies.finger\_trap](#aiomql.lib.strategies.finger_trap)
* [Entry](#aiomql.lib.strategies.finger_trap.Entry)
* [aiomql.lib.strategies](#aiomql.lib.strategies)
+* [aiomql.lib.symbols.crypto\_symbol](#aiomql.lib.symbols.crypto_symbol)
+ * [CryptoSymbol](#aiomql.lib.symbols.crypto_symbol.CryptoSymbol)
+ * [compute\_volume](#aiomql.lib.symbols.crypto_symbol.CryptoSymbol.compute_volume)
* [aiomql.lib.symbols.forex\_symbol](#aiomql.lib.symbols.forex_symbol)
* [ForexSymbol](#aiomql.lib.symbols.forex_symbol.ForexSymbol)
- * [pip](#aiomql.lib.symbols.forex_symbol.ForexSymbol.pip)
* [compute\_volume](#aiomql.lib.symbols.forex_symbol.ForexSymbol.compute_volume)
* [aiomql.lib.symbols](#aiomql.lib.symbols)
-* [aiomql.lib.traders.simple\_deal\_trader](#aiomql.lib.traders.simple_deal_trader)
- * [DealTrader](#aiomql.lib.traders.simple_deal_trader.DealTrader)
- * [create\_order](#aiomql.lib.traders.simple_deal_trader.DealTrader.create_order)
* [aiomql.lib.traders](#aiomql.lib.traders)
* [aiomql.lib](#aiomql.lib)
* [aiomql.order](#aiomql.order)
@@ -144,12 +146,12 @@
* [\_\_init\_\_](#aiomql.positions.Positions.__init__)
* [positions\_total](#aiomql.positions.Positions.positions_total)
* [positions\_get](#aiomql.positions.Positions.positions_get)
+ * [close](#aiomql.positions.Positions.close)
* [close\_all](#aiomql.positions.Positions.close_all)
* [aiomql.ram](#aiomql.ram)
* [RAM](#aiomql.ram.RAM)
* [\_\_init\_\_](#aiomql.ram.RAM.__init__)
* [get\_amount](#aiomql.ram.RAM.get_amount)
- * [get\_volume](#aiomql.ram.RAM.get_volume)
* [aiomql.records](#aiomql.records)
* [Records](#aiomql.records.Records)
* [\_\_init\_\_](#aiomql.records.Records.__init__)
@@ -163,6 +165,18 @@
* [\_\_init\_\_](#aiomql.result.Result.__init__)
* [to\_csv](#aiomql.result.Result.to_csv)
* [save\_csv](#aiomql.result.Result.save_csv)
+* [aiomql.sessions](#aiomql.sessions)
+ * [delta](#aiomql.sessions.delta)
+ * [Session](#aiomql.sessions.Session)
+ * [\_\_init\_\_](#aiomql.sessions.Session.__init__)
+ * [begin](#aiomql.sessions.Session.begin)
+ * [close](#aiomql.sessions.Session.close)
+ * [action](#aiomql.sessions.Session.action)
+ * [until](#aiomql.sessions.Session.until)
+ * [Sessions](#aiomql.sessions.Sessions)
+ * [find](#aiomql.sessions.Sessions.find)
+ * [find\_next](#aiomql.sessions.Sessions.find_next)
+ * [check](#aiomql.sessions.Sessions.check)
* [aiomql.strategy](#aiomql.strategy)
* [Strategy](#aiomql.strategy.Strategy)
* [\_\_init\_\_](#aiomql.strategy.Strategy.__init__)
@@ -178,7 +192,10 @@
* [book\_add](#aiomql.symbol.Symbol.book_add)
* [book\_get](#aiomql.symbol.Symbol.book_get)
* [book\_release](#aiomql.symbol.Symbol.book_release)
+ * [check\_volume](#aiomql.symbol.Symbol.check_volume)
+ * [round\_off\_volume](#aiomql.symbol.Symbol.round_off_volume)
* [compute\_volume](#aiomql.symbol.Symbol.compute_volume)
+ * [convert\_currency](#aiomql.symbol.Symbol.convert_currency)
* [currency\_conversion](#aiomql.symbol.Symbol.currency_conversion)
* [copy\_rates\_from](#aiomql.symbol.Symbol.copy_rates_from)
* [copy\_rates\_from\_pos](#aiomql.symbol.Symbol.copy_rates_from_pos)
@@ -205,6 +222,9 @@
* [\_\_init\_\_](#aiomql.trader.Trader.__init__)
* [create\_order](#aiomql.trader.Trader.create_order)
* [set\_order\_limits](#aiomql.trader.Trader.set_order_limits)
+ * [set\_trade\_stop\_levels](#aiomql.trader.Trader.set_trade_stop_levels)
+ * [check\_order](#aiomql.trader.Trader.check_order)
+ * [record\_trade](#aiomql.trader.Trader.record_trade)
* [place\_trade](#aiomql.trader.Trader.place_trade)
* [aiomql.utils](#aiomql.utils)
* [dict\_to\_string](#aiomql.utils.dict_to_string)
@@ -355,7 +375,7 @@ The bot class. Create a bot instance to run your strategies.
- `account` _Account_ - Account Object.
- `executor` - The default thread executor.
-- `symbols` _set[Symbols]_ - A set of symbols for the trading session
+- `symbols` _list[Symbols]_ - A set of symbols for the trading session
@@ -371,6 +391,37 @@ Prepares the bot by signing in to the trading account and initializing the symbo
SystemExit if sign in was not successful
+
+
+#### add\_function
+
+```python
+def add_function(func: Callable, **kwargs: dict)
+```
+
+Add a function to the executor.
+
+**Arguments**:
+
+- `func` _Callable_ - A function to be executed
+- `**kwargs` _dict_ - Keyword arguments for the function
+
+
+
+#### add\_coroutine
+
+```python
+def add_coroutine(coro: Coroutine, **kwargs)
+```
+
+Add a coroutine to the executor.
+
+**Arguments**:
+
+- `coro` _Coroutine_ - A coroutine to be executed
+- `**kwargs` _dict_ - keyword arguments for the coroutine
+
+
#### execute
@@ -460,7 +511,7 @@ async def init_symbol(symbol: Symbol) -> Symbol
Initialize a symbol before the beginning of a trading sessions.
Removes it from the list of symbols if it was not successfully initialized or not available
-for the current market.
+for the account.
**Arguments**:
@@ -2223,6 +2274,8 @@ Executor class for running multiple strategies on multiple symbols concurrently.
- `executor` _ThreadPoolExecutor_ - The executor object.
- `workers` _list_ - List of strategies.
+- `coroutines` _dict[Coroutine, dict]_ - A dictionary of coroutines to run in the executor
+- `functions` _dict[Callable, dict]_ - A dictionary of functions to run in the executor
@@ -2243,15 +2296,11 @@ Add multiple strategies at once
#### remove\_workers
```python
-def remove_workers(*symbols: Sequence[Symbol])
+def remove_workers()
```
Removes any worker running on a symbol not successfully initialized.
-**Arguments**:
-
-- `*symbols` - Successfully initialized symbols.
-
#### add\_worker
@@ -2266,13 +2315,13 @@ Add a strategy instance to the list of workers
- `strategy` _Strategy_ - A strategy object
-
+
-#### run
+#### trade
```python
@staticmethod
-def run(strategy: type(Strategy))
+def trade(strategy: type(Strategy))
```
Wraps the coroutine trade method of each strategy with 'asyncio.run'.
@@ -2281,6 +2330,21 @@ Wraps the coroutine trade method of each strategy with 'asyncio.run'.
- `strategy` _Strategy_ - A strategy object
+
+
+#### run
+
+```python
+def run(func, kwargs: dict)
+```
+
+Run a coroutine function
+
+**Arguments**:
+
+- `func` - The coroutine. A variadic function.
+- `kwargs` - A dictionary of keyword arguments for the function
+
#### execute
@@ -2333,8 +2397,8 @@ The history class handles completed trade deals and trade orders in the trading
```python
def __init__(*,
- date_from: datetime | float = 0,
- date_to: datetime | float = 0,
+ date_from: datetime | float = None,
+ date_to: datetime | float = None,
group: str = "",
ticket: int = 0,
position: int = 0)
@@ -2441,7 +2505,7 @@ Get total number of orders within the specified period in the constructor.
class Entry()
```
-Entry class for FingerTrap strategy.Will be used to store entry conditions and other entry related data.
+Entry class for FingerTrap strategy. Will be used to store entry conditions and other entry related data.
**Attributes**:
@@ -2450,15 +2514,53 @@ Entry class for FingerTrap strategy.Will be used to store entry conditions and o
- `ranging` _bool_ - True if the market is ranging
- `snooze` _float_ - Time to wait before checking for entry conditions
- `trend` _str_ - The current trend of the market
-- `last_candle` _Candle_ - The last candle of the market
- `new` _bool_ - True if the last candle is new
- `order_type` _OrderType_ - The type of order to place
-- `pips` _int_ - The number of pips to place the order from the current price
# aiomql.lib.strategies
+
+
+# aiomql.lib.symbols.crypto\_symbol
+
+
+
+## CryptoSymbol Objects
+
+```python
+class CryptoSymbol(Symbol)
+```
+
+Subclass of Symbol for Crypto/Fiat Symbols. Handles the computation of volume based on the amount to risk.
+
+
+
+#### compute\_volume
+
+```python
+async def compute_volume(*, amount: float, points, use_limits=False) -> float
+```
+
+Compute volume given an amount to risk and target pips. Round the computed volume to the nearest step.
+
+**Arguments**:
+
+- `amount` _float_ - Amount to risk. Given in terms of the account currency.
+- `points` _float_ - Target pips.
+- `use_limits` _bool_ - If True, the computed volume checked against the maximum and minimum volume.
+
+
+**Returns**:
+
+- `float` - volume
+
+
+**Raises**:
+
+- `VolumeError` - If the computed volume is less than the minimum volume or greater than the maximum volume.
+
# aiomql.lib.symbols.forex\_symbol
@@ -2474,30 +2576,12 @@ class ForexSymbol(Symbol)
Subclass of Symbol for Forex Symbols. Handles the conversion of currency and the computation of stop loss,
take profit and volume.
-
-
-#### pip
-
-```python
-@property
-def pip()
-```
-
-Returns the pip value of the symbol. This is ten times the point value for forex symbols.
-
-**Returns**:
-
-- `float` - The pip value of the symbol.
-
#### compute\_volume
```python
-async def compute_volume(*,
- amount: float,
- pips: float,
- use_minimum: bool = True) -> float
+async def compute_volume(*, amount: float, pips, use_limits=False) -> float
```
Compute volume given an amount to risk and target pips. Round the computed volume to the nearest step.
@@ -2506,11 +2590,7 @@ Compute volume given an amount to risk and target pips. Round the computed volum
- `amount` _float_ - Amount to risk. Given in terms of the account currency.
- `pips` _float_ - Target pips.
-
-
-**Arguments**:
-
-- `use_minimum` _bool_ - If True, the minimum volume is returned if the computed volume is less than the minimum volume.
+- `use_limits` _bool_ - If True, the computed volume checked against the maximum and minimum volume.
**Returns**:
@@ -2526,36 +2606,6 @@ Compute volume given an amount to risk and target pips. Round the computed volum
# aiomql.lib.symbols
-
-
-# aiomql.lib.traders.simple\_deal\_trader
-
-
-
-## DealTrader Objects
-
-```python
-class DealTrader(Trader)
-```
-
-A base class for placing trades based on the number of pips to target
-
-
-
-#### create\_order
-
-```python
-async def create_order(*, order_type: OrderType, pips: float = 0)
-```
-
-Using the number of target pips it determines the lot size, stop loss and take profit for the order,
-and updates the order object with the values.
-
-**Arguments**:
-
-- `order_type` _OrderType_ - Type of order
-- `pips` _float_ - Target pips
-
# aiomql.lib.traders
@@ -2770,25 +2820,50 @@ Get the number of open positions.
#### positions\_get
```python
-async def positions_get()
+async def positions_get(symbol: str = '', group: str = '', ticket: int = 0)
```
Get open positions with the ability to filter by symbol or ticket.
+**Arguments**:
+
+- `symbol` _str_ - Financial instrument name.
+- `group` _str_ - The filter for arranging a group of necessary symbols. Optional named parameter. If the group
+ is specified, the function returns only positions meeting a specified criteria for a symbol name.
+- `ticket` _int_ - Position ticket
+
+
**Returns**:
- `list[TradePosition]` - A list of open trade positions
+
+
+#### close
+
+```python
+async def close(*, ticket: int, symbol: str, price: float, volume: float,
+ order_type: OrderType)
+```
+
+Close an open position for the trading account.
+
#### close\_all
```python
-async def close_all() -> int
+async def close_all(symbol: str = '', group: str = '') -> int
```
Close all open positions for the trading account.
+**Arguments**:
+
+- `symbol` _str_ - Financial instrument name.
+- `group` _str_ - The filter for specifying a group of symbols.
+
+
**Returns**:
- `int` - Return number of positions closed.
@@ -2812,21 +2887,21 @@ class RAM()
#### \_\_init\_\_
```python
-def __init__(**kwargs)
+def __init__(*,
+ risk_to_reward: float = 1,
+ risk: float = 0.01,
+ amount: float = 0,
+ **kwargs)
```
-Risk Assessment and Management. All provided keyword arguments are set as attributes.
+Initialize Risk Assessment and Management with the provided keyword arguments.
**Arguments**:
-- `kwargs` _Dict_ - Keyword arguments.
-
- Defaults:
-- `risk_to_reward` _float_ - Risk to reward ratio 1
+- `risk_to_reward` _float_ - Risk to reward ratio. Defaults to 1
- `risk` _float_ - Percentage of account balance to risk per trade 0.01 # 1%
- `amount` _float_ - Amount to risk per trade in terms of account currency 0
-- `pips` _float_ - Target pips 0
-- `volume` _float_ - Volume to trade 0
+- `kwargs` - extra keyword arguments are set as object attributes
@@ -2836,7 +2911,7 @@ Risk Assessment and Management. All provided keyword arguments are set as attrib
async def get_amount(risk: float = 0) -> float
```
-Calculate the amount to risk per trade as a percentage of free margin.
+Calculate the amount to risk per trade as a percentage of equity.
**Arguments**:
@@ -2847,35 +2922,6 @@ Calculate the amount to risk per trade as a percentage of free margin.
- `float` - Amount to risk per trade
-
-
-#### get\_volume
-
-```python
-async def get_volume(*,
- symbol: Symbol,
- pips: float = 0,
- amount: float = 0) -> float
-```
-
-Calculate the volume to trade. if pips is not provided, the pips attribute is used.
-If the amount attribute or amount argument is zero, the amount is calculated using the get_amount method based on the risk.
-
-**Arguments**:
-
-- `symbol` _Symbol_ - Financial instrument
-
-
-**Arguments**:
-
-- `pips` _float_ - Target pips. Defaults to zero.
-- `amount` _float_ - Amount to risk per trade. Defaults to zero.
-
-
-**Returns**:
-
-- `float` - Volume to trade
-
# aiomql.records
@@ -2906,7 +2952,8 @@ This utility class read trade records from csv files, and update them based on t
def __init__(records_dir: Path = '')
```
-Initialize the Records class.
+Initialize the Records class. The main method of this class is update_records which you should call to update
+all the records specified in the records_dir.
**Arguments**:
@@ -3020,7 +3067,7 @@ Prepare result data
#### to\_csv
```python
-async def to_csv()
+def to_csv()
```
Record trade results and associated parameters as a csv file
@@ -3035,6 +3082,201 @@ async def save_csv()
Save trade results and associated parameters as a csv file in a separate thread
+
+
+# aiomql.sessions
+
+Sessions allow you to run code at specific times of the day.
+
+
+
+#### delta
+
+```python
+def delta(obj: time)
+```
+
+Get the timedelta of a datetime.time object.
+
+**Arguments**:
+
+- `obj` _datetime.time_ - A datetime.time object.
+
+
+
+## Session Objects
+
+```python
+class Session()
+```
+
+A session is a time period between two datetime.time objects specified in utc.
+
+**Attributes**:
+
+- `start` _datetime.time_ - The start time of the session.
+- `end` _datetime.time_ - The end time of the session.
+- `on_start` _str_ - The action to take when the session starts. Default is None.
+- `on_end` _str_ - The action to take when the session ends. Default is None.
+- `custom_start` _Callable_ - A custom function to call when the session starts. Default is None.
+- `custom_end` _Callable_ - A custom function to call when the session ends. Default is None.
+- `name` _str_ - A name for the session. Default is a combination of start and end.
+
+
+**Methods**:
+
+- `begin` - Call the action specified in on_start or custom_start.
+- `close` - Call the action specified in on_end or custom_end.
+- `action` - Used by begin and close to call the action specified.
+- `delta` - Get the timedelta of a datetime.time object.
+- `until` - Get the seconds until the session starts from the current time.
+
+
+
+#### \_\_init\_\_
+
+```python
+def __init__(*,
+ start: int | time,
+ end: int | time,
+ on_start: Literal['close_all', 'close_win', 'close_loss',
+ 'custom_start'] = None,
+ on_end: Literal['close_all', 'close_win', 'close_loss',
+ 'custom_end'] = None,
+ custom_start: Callable = None,
+ custom_end: Callable = None,
+ name: str = '')
+```
+
+Create a session.
+
+**Arguments**:
+
+- `start` _int | datetime.time_ - The start time of the session in UTC.
+- `end` _int | datetime.time_ - The end time of the session in UTC.
+- `on_start` _Literal['close_all', 'close_win', 'close_loss', 'custom_start']_ - The action to take when the
+ session starts. Default is None.
+- `on_end` _Literal['close_all', 'close_win', 'close_loss', 'custom_end']_ - The action to take when the session
+ ends. Default is None.
+- `custom_start` _Callable_ - A custom function to call when the session starts. Default is None.
+- `custom_end` _Callable_ - A custom function to call when the session ends. Default is None.
+- `name` _str_ - A name for the session. Default is a combination of start and end.
+
+
+
+#### begin
+
+```python
+async def begin()
+```
+
+Call the action specified in on_start or custom_start.
+
+
+
+#### close
+
+```python
+async def close()
+```
+
+Call the action specified in on_end or custom_end.
+
+
+
+#### action
+
+```python
+async def action(action)
+```
+
+Used by begin and close to call the action specified.
+
+**Arguments**:
+
+- `action` _Literal['close_all', 'close_win', 'close_loss', 'custom_start', 'custom_end']_ - The action to take.
+
+
+
+#### until
+
+```python
+def until()
+```
+
+Get the seconds until the session starts from the current time in seconds.
+
+
+
+## Sessions Objects
+
+```python
+class Sessions()
+```
+
+Sessions allow you to run code at specific times of the day. It is a collection of Session objects.
+Sessions are sorted by start time. The sessions object is an asynchronous context manager.
+
+**Attributes**:
+
+- `sessions` _list[Session]_ - A list of Session objects.
+- `current_session` _Session_ - The current session.
+
+
+**Methods**:
+
+- `find` - Find a session that contains a datetime.time object.
+- `find_next` - Find the next session that contains a datetime.time object.
+- `check` - Check if the current session has started and if not, wait until it starts.
+
+
+
+#### find
+
+```python
+def find(obj: time) -> Session | None
+```
+
+Find a session that contains a datetime.time object.
+
+**Arguments**:
+
+- `obj` _datetime.time_ - A datetime.time object.
+
+
+**Returns**:
+
+ Session | None: A Session object or None if not found.
+
+
+
+#### find\_next
+
+```python
+def find_next(obj: time) -> Session
+```
+
+Find the next session that contains a datetime.time object.
+
+**Arguments**:
+
+- `obj` _datetime.time_ - A datetime.time object.
+
+
+**Returns**:
+
+- `Session` - A Session object.
+
+
+
+#### check
+
+```python
+async def check()
+```
+
+Check if the current session has started and if not, wait until it starts.
+
# aiomql.strategy
@@ -3072,7 +3314,10 @@ The base class for creating strategies.
#### \_\_init\_\_
```python
-def __init__(*, symbol: Symbol, params: dict = None)
+def __init__(*,
+ symbol: Symbol,
+ params: dict = None,
+ sessions: Sessions = None)
```
Initiate the parameters dict and add name and symbol fields.
@@ -3281,36 +3526,75 @@ Cancels subscription of the MetaTrader 5 terminal to the Market Depth change eve
- `bool` - True if successful, otherwise – False.
+
+
+#### check\_volume
+
+```python
+def check_volume(volume) -> tuple[bool, float]
+```
+
+Check if the volume is within the limits of the symbol. If not, return the nearest limit.
+
+**Arguments**:
+
+- `volume` _float_ - Volume to check
+
+- `Returns` - tuple[bool, float]: Returns a tuple of a boolean and a float. The boolean indicates if the volume is
+ within the limits of the symbol. The float is the volume to use if the volume is not within the limits of the
+ symbol.
+
+
+
+#### round\_off\_volume
+
+```python
+def round_off_volume(volume) -> float
+```
+
+Round off the volume to the nearest volume step.
+
+**Arguments**:
+
+- `volume` _float_ - Volume to round off
+
+
+**Returns**:
+
+- `float` - Rounded off volume
+
#### compute\_volume
```python
-async def compute_volume(*,
- amount: float,
- pips: float,
- use_minimum: bool = True) -> float
+async def compute_volume(*args, **kwargs) -> float
```
-Computes the volume of a trade based on the amount and the number of pips to target.
+Computes the volume required for a trade usually based on the amount and any other keyword arguments.
This is a dummy method that returns the minimum volume of the symbol. It is meant to be overridden by a subclass
-Checkout Forex Symbol implementation in srciomql\lib\ForexSymbol.py
+that implements the computation of volume.
**Arguments**:
-- `amount` _float_ - Amount to risk in the trade
-- `pips` _float_ - Number of pips to target
-
-
-**Arguments**:
-
-- `use_minimum` _bool_ - If True, the minimum volume is returned if the computed volume is less than the minimum volume.
+- `use_limits` _bool_ - round up or round down the computed volume to the nearest volume limit i.e volume_min
+ or volume_max
**Returns**:
- `float` - Returns the volume of the trade
+
+
+#### convert\_currency
+
+```python
+async def convert_currency(*, amount: float, base: str, quote: str) -> float
+```
+
+Convert from one currency to the other. Alias for currency_conversion
+
#### currency\_conversion
@@ -3331,7 +3615,7 @@ Convert from one currency to the other.
**Returns**:
-- `float` - Amount in terms of the base currency or None if it failed to convert
+- `float` - Amount in terms of the base currency
**Raises**:
@@ -3351,15 +3635,13 @@ async def copy_rates_from(*,
Get bars from the MetaTrader 5 terminal starting from the specified date.
-**Arguments**:
+Args: timeframe (TimeFrame): Timeframe the bars are requested for. Set by a value from the TimeFrame
+enumeration. Required unnamed parameter.
-- `timeframe` _TimeFrame_ - Timeframe the bars are requested for. Set by a value from the TimeFrame enumeration. Required unnamed parameter.
-
-- `date_from` _datetime | int_ - Date of opening of the first bar from the requested sample. Set by the 'datetime' object or as a number
- of seconds elapsed since 1970.01.01. Required unnamed parameter.
-
-- `count` _int_ - Number of bars to receive. Required unnamed parameter.
-
+date_from (datetime | int): Date of opening of the first bar from the requested sample. Set by the
+'datetime' object or as a number of seconds elapsed since 1970.01.01. Required unnamed parameter.
+
+count (int): Number of bars to receive. Required unnamed parameter.
**Returns**:
@@ -3776,7 +4058,6 @@ async def create_order(*, order_type: OrderType, **kwargs)
```
Complete the order object with the required values. Creates a simple order.
-Uses the ram instance to set the volume.
**Arguments**:
@@ -3791,13 +4072,50 @@ Uses the ram instance to set the volume.
async def set_order_limits(pips: float)
```
-Sets the stop loss and take profit for the order.
-This method uses pips as defined for forex instruments.
+Sets the stop loss and take profit for the order. This method uses pips as defined for forex instruments.
**Arguments**:
- `pips` - Target pips
+
+
+#### set\_trade\_stop\_levels
+
+```python
+async def set_trade_stop_levels(*, points)
+```
+
+Set the stop loss and take profit levels of the order based on the points.
+
+
+
+#### check\_order
+
+```python
+async def check_order() -> bool
+```
+
+Check order before sending it to the broker.
+
+**Returns**:
+
+- `bool` - True if order can go through else false
+
+
+
+#### record\_trade
+
+```python
+async def record_trade(result: OrderSendResult)
+```
+
+Record the trade in a csv file.
+
+**Arguments**:
+
+- `result` _OrderSendResult_ - Result of the order send
+
#### place\_trade
@@ -3811,7 +4129,7 @@ Places a trade based on the order_type.
**Arguments**:
- `order_type` _OrderType_ - Type of order
-- `params` - parameters to be saved with the trade
+- `params` - parameters of the trading strategy used to place the trade
- `kwargs` - keyword arguments as required for the specific trader
diff --git a/docs/ram.md b/docs/ram.md
index 682feaf..8590900 100644
--- a/docs/ram.md
+++ b/docs/ram.md
@@ -1,83 +1,39 @@
-
-
-# aiomql.ram
-
-Risk Assessment and Management
-
-
-
-## RAM Objects
+## Risk Assessment and Management
```python
class RAM()
```
-
-
-
-#### \_\_init\_\_
+### \_\_init\_\_
```python
def __init__(**kwargs)
```
-
Risk Assessment and Management. All provided keyword arguments are set as attributes.
-**Arguments**:
+#### Parameters
+| Name | Type | Description | Default |
+|----------------|------|-------------------------------------------------------|---------|
+| risk_to_reward | float | Risk to reward ratio | 1|
+| risk | float | Percentage of account balance to risk per trade | 0.01 # 1%|
+| amount | float | Amount to risk per trade in terms of account currency | 0|
+| **kwargs** | Dict | Keyword arguments to be set as object attributes | {} |
-- `kwargs` _Dict_ - Keyword arguments.
-
- Defaults:
-- `risk_to_reward` _float_ - Risk to reward ratio 1
-- `risk` _float_ - Percentage of account balance to risk per trade 0.01 # 1%
-- `amount` _float_ - Amount to risk per trade in terms of account currency 0
-- `pips` _float_ - Target pips 0
-- `volume` _float_ - Volume to trade 0
-#### get\_amount
+### get\_amount
```python
async def get_amount(risk: float = 0) -> float
```
-
Calculate the amount to risk per trade as a percentage of free margin.
-**Arguments**:
-
-- `risk` _float_ - Percentage of account balance to risk per trade. Defaults to zero.
-
-
-**Returns**:
-
-- `float` - Amount to risk per trade
-
-
-
-#### get\_volume
-
-```python
-async def get_volume(*,
- symbol: Symbol,
- pips: float = 0,
- amount: float = 0) -> float
-```
-
-Calculate the volume to trade. if pips is not provided, the pips attribute is used.
-If the amount attribute or amount argument is zero, the amount is calculated using the get_amount method based on the risk.
-
-**Arguments**:
-
-- `symbol` _Symbol_ - Financial instrument
-
-
-**Arguments**:
-
-- `pips` _float_ - Target pips. Defaults to zero.
-- `amount` _float_ - Amount to risk per trade. Defaults to zero.
-
-
-**Returns**:
-
-- `float` - Volume to trade
+#### Parameters
+| Name | Type | Description | Default |
+|----------------|------|-------------------------------------------------------|---------|
+| risk | float | Percentage of account balance to risk per trade | 0.01 # 1%|
+#### Returns
+| Name | Type | Description |
+|----------------|------|-------------------------------------------------------|
+| amount | float | Amount to risk per trade in terms of account currency |
diff --git a/docs/sessions.md b/docs/sessions.md
index c299bff..e9c2be6 100644
--- a/docs/sessions.md
+++ b/docs/sessions.md
@@ -17,7 +17,6 @@ A session is a time period between two datetime.time objects specified in utc.
|**custom_start**| **Callable** | A custom function to call when the session starts. Default is None. | None |
|**custom_end**| **Callable** | A custom function to call when the session ends. Default is None. | None |
|**name**| **str** | The name of the session. Default is a combination of start and finish. | |
-|**seconds**| **set[int]** | The set of seconds in the session. | None |
### Methods:
|Name|Description|
diff --git a/docs/symbol.md b/docs/symbol.md
index 9bec305..3a0bcb7 100644
--- a/docs/symbol.md
+++ b/docs/symbol.md
@@ -295,7 +295,7 @@ async def copy_ticks_range(*,
flags: CopyTicks = CopyTicks.ALL) -> Ticks
```
Get ticks for the specified date range from the MetaTrader 5 terminal.
-#### Arguments:
+#### Parameters:
|Name| Type | Description | Default |
|---|--------------------|-----------------------------|-------------------|
|**date_from**| **datetime, int** | Date the ticks are requested from. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. | Required unnamed parameter |
@@ -310,13 +310,24 @@ Get ticks for the specified date range from the MetaTrader 5 terminal.
|---|---|
|**ValueError**|If request was unsuccessful and None was returned|
+### check_volume
+```python
+async def check_volume(*, volume: float) -> tuple[bool, float]
+```
+Checks if the volume is within the minimum and maximum volume for the symbol. If not, return the nearest limit.
+
+### round_off_volume
+```python
+async def round_off_volume(*, volume: float) -> float
+```
+Rounds off the volume to the nearest minimum or maximum volume for the symbol.
+
+
+
### compute_volume
```python
-async def compute_volume(*,
- amount: float,
- pips: float,
- use_limits: bool = True) -> float
+async def compute_volume(*args, **kwargs) -> float
```
-Computes the volume of a trade based on the amount and the number of pips to target.
+Computes the volume of a trade based on the amount and other parameters.
This is a dummy method that returns the minimum volume of the symbol. It is meant to be overridden by a subclass
Checkout Forex Symbol implementation in [ForexSymbol](#forexsymbol)
diff --git a/examples/bot.py b/examples/bot.py
index 63ad380..975efe5 100644
--- a/examples/bot.py
+++ b/examples/bot.py
@@ -22,7 +22,8 @@ def build_bot():
sess = Session(name='London', start=8, end=time(hour=15, minute=30), on_end='close_all')
sess2 = Session(name='New York', start=13, end=time(hour=20, minute=30))
sess3 = Session(name='Tokyo', start=23, end=time(hour=6, minute=30))
- sessions = Sessions(sess, sess2, sess3)
+ allsess = Session(name='All', start=0, end=23, on_end='close_all')
+ sessions = Sessions(sess, sess2, sess3, allsess)
# configurable parameters for the strategy
params = {'trend_candles_count': 500, 'fast_period': 8}
diff --git a/pyproject.toml b/pyproject.toml
index f1590cd..8f4e01c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aiomql"
-version = "3.0.6"
+version = "3.0.7"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
diff --git a/src/aiomql/candle.py b/src/aiomql/candle.py
index ffc4641..da0d4b6 100644
--- a/src/aiomql/candle.py
+++ b/src/aiomql/candle.py
@@ -11,6 +11,7 @@ from .core.constants import TimeFrame
logger = getLogger(__name__)
+
class Candle:
"""A class representing bars from the MetaTrader 5 terminal as a customized class analogous to Japanese Candlesticks.
You can subclass this class for added customization.
@@ -45,6 +46,7 @@ class Candle:
self.time = kwargs.pop('time', 0)
self.Index = kwargs.pop('Index', 0)
self.set_attributes(**kwargs)
+
def __repr__(self):
keys = reprlib.repr(', '.join('%s=%s' % (i, j) for i, j in self.__dict__.items()))[1:-1]
return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': keys}
diff --git a/src/aiomql/lib/__init__.py b/src/aiomql/lib/__init__.py
new file mode 100644
index 0000000..130b456
--- /dev/null
+++ b/src/aiomql/lib/__init__.py
@@ -0,0 +1,3 @@
+from .strategies import *
+from .traders import *
+from .symbols import *
diff --git a/src/aiomql/lib/strategies/__init__.py b/src/aiomql/lib/strategies/__init__.py
new file mode 100644
index 0000000..cb41d25
--- /dev/null
+++ b/src/aiomql/lib/strategies/__init__.py
@@ -0,0 +1 @@
+from .finger_trap import FingerTrap
\ No newline at end of file
diff --git a/src/aiomql/lib/strategies/finger_trap.py b/src/aiomql/lib/strategies/finger_trap.py
new file mode 100644
index 0000000..4c100dd
--- /dev/null
+++ b/src/aiomql/lib/strategies/finger_trap.py
@@ -0,0 +1,210 @@
+import asyncio
+import logging
+from typing import Literal
+from dataclasses import dataclass
+
+from ...symbol import Symbol
+from ...trader import Trader
+from ...candle import Candles
+from ...strategy import Strategy
+from ...core import TimeFrame, OrderType
+from ...sessions import Sessions
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class Entry:
+ """
+ Entry class for FingerTrap strategy. Will be used to store entry conditions and other entry related data.
+
+ Attributes:
+ bearish (bool): True if the market is bearish
+ bullish (bool): True if the market is bullish
+ ranging (bool): True if the market is ranging
+ snooze (float): Time to wait before checking for entry conditions
+ trend (str): The current trend of the market
+ new (bool): True if the last candle is new
+ order_type (OrderType): The type of order to place
+ """
+
+ bearish: bool = False
+ bullish: bool = False
+ ranging: bool = True
+ trending: bool = False
+ trend: Literal["ranging", "bullish", "bearish"] = "ranging"
+ snooze: float = 0
+ last_trend_time: float = 0
+ last_entry_time: float = 0
+ new: bool = True
+ order_type: OrderType | None = None
+
+ def update(self, **kwargs):
+ fields = self.__dict__
+ for key in kwargs:
+ if key in fields:
+ setattr(self, key, kwargs[key])
+ match self.trend:
+ case "ranging":
+ self.ranging = True
+ self.trending = self.bullish = self.bearish = False
+ case "bullish":
+ self.ranging = self.bearish = False
+ self.bullish = self.trending = True
+ case "bearish":
+ self.ranging = self.bullish = False
+ self.bearish = self.trending = True
+
+
+class FingerTrap(Strategy):
+ trend_time_frame: TimeFrame
+ entry_time_frame: TimeFrame
+ trend: int
+ fast_period: int
+ slow_period: int
+ entry_period: int
+ parameters: dict
+ prices: Candles
+ name = "FingerTrap"
+ interval: TimeFrame
+ entry_candles_count: int
+ trend_candles_count: int
+
+ def __init__(
+ self,
+ *,
+ symbol: Symbol,
+ params: dict | None = None,
+ trader: Trader = None,
+ sessions: Sessions = None,
+ ):
+ super().__init__(symbol=symbol, params=params, sessions=sessions)
+ self.trend = self.parameters.get("trend", 3)
+ self.fast_period = self.parameters.setdefault("fast_period", 8)
+ self.slow_period = self.parameters.setdefault("slow_period", 34)
+ self.entry_time_frame = self.parameters.setdefault(
+ "entry_time_frame", TimeFrame.M5
+ )
+ self.trend_time_frame = self.parameters.setdefault(
+ "trend_time_frame", TimeFrame.H1
+ )
+ self.trader = trader or Trader(symbol=self.symbol)
+ self.entry: Entry = Entry(snooze=self.trend_time_frame.time)
+ self.entry_period = self.parameters.setdefault("entry_period", 8)
+
+ self.trend_candles_count = self.parameters.setdefault(
+ "trend_candles_count", 86400 // self.trend_time_frame.time
+ )
+ self.trend_candles_count = max(self.trend_candles_count, self.slow_period)
+ self.entry_candles_count = self.trend_candles_count * (
+ self.trend_time_frame.time // self.entry_time_frame.time
+ )
+ self.entry_candles_count = max(self.entry_candles_count, self.entry_period)
+
+ async def check_trend(self):
+ try:
+ candles = await self.symbol.copy_rates_from_pos(
+ timeframe=self.trend_time_frame, count=self.trend_candles_count
+ )
+ current = candles[-1]
+ if current.time > self.entry.last_trend_time:
+ self.entry.update(new=True, last_trend_time=current.time)
+ else:
+ self.entry.update(new=False)
+ return
+
+ candles.ta.ema(length=self.slow_period, append=True, fillna=0)
+ candles.ta.ema(length=self.fast_period, append=True, fillna=0)
+ candles.rename(
+ inplace=True,
+ **{
+ f"EMA_{self.fast_period}": "fast",
+ f"EMA_{self.slow_period}": "slow",
+ },
+ )
+
+ # Compute
+ candles["fast_A_slow"] = candles.ta_lib.above(candles.fast, candles.slow)
+ candles["fast_B_slow"] = candles.ta_lib.below(candles.fast, candles.slow)
+ candles["close_A_fast"] = candles.ta_lib.above(candles.close, candles.fast)
+ candles["close_B_fast"] = candles.ta_lib.below(candles.close, candles.fast)
+
+ trend = candles[-self.trend : -1]
+ if all(
+ (c.is_bullish() and c.fast_A_slow and c.close_A_fast) for c in trend
+ ):
+ self.entry.update(trend="bullish")
+
+ elif all(
+ c.is_bearish() and c.fast_B_slow and c.close_B_fast for c in trend
+ ):
+ self.entry.update(trend="bearish")
+
+ else:
+ self.entry.update(trend="ranging", snooze=self.trend_time_frame.time)
+ except Exception as exe:
+ logger.error(f"{exe}. Error in {self.__class__.__name__}.check_trend")
+
+ async def confirm_trend(self):
+ try:
+ candles = await self.symbol.copy_rates_from_pos(
+ timeframe=self.entry_time_frame, count=self.entry_candles_count
+ )
+ current = candles[-1]
+ if current.time > self.entry.last_entry_time:
+ self.entry.update(new=True, last_entry_time=current.time)
+ else:
+ self.entry.update(new=False)
+ return
+
+ candles.ta.ema(length=self.entry_period, append=True, fillna=0)
+ candles.rename(**{f"EMA_{self.entry_period}": "ema"})
+ candles["close_A_ema"] = candles.ta_lib.above(candles.close, candles.ema)
+ candles["close_B_ema"] = candles.ta_lib.below(candles.close, candles.ema)
+ candles["close_XA_ema"] = candles.ta_lib.cross(candles.close, candles.ema)
+ candles["close_XB_ema"] = candles.ta_lib.cross(
+ candles.close, candles.ema, above=False
+ )
+ if self.entry.bullish and current.close_XA_ema:
+ self.entry.update(
+ snooze=self.entry_time_frame.time, order_type=OrderType.BUY
+ )
+ elif self.entry.bearish and current.close_XB_ema:
+ self.entry.update(
+ snooze=self.entry_time_frame.time, order_type=OrderType.SELL
+ )
+ else:
+ self.entry.update(snooze=self.entry_time_frame.time, order_type=None)
+ except Exception as exe:
+ logger.error(f"{exe} Error in {self.__class__.__name__}.confirm_trend")
+
+ async def watch_market(self):
+ await self.check_trend()
+ if not self.entry.ranging:
+ await self.confirm_trend()
+
+ async def trade(self):
+ logger.info(f"Trading {self.symbol}")
+ async with self.sessions as sess:
+ while True:
+ await sess.check()
+ try:
+ await self.watch_market()
+ if not self.entry.new:
+ await asyncio.sleep(2)
+ continue
+ if self.entry.order_type is None:
+ await self.sleep(self.entry.snooze)
+ continue
+
+ await self.trader.place_trade(
+ order_type=self.entry.order_type, params=self.parameters
+ )
+ await self.sleep(self.entry.snooze)
+ except Exception as err:
+ logger.error(
+ f"Error: {err}\t Symbol: {self.symbol} in {self.__class__.__name__}.trade"
+ )
+ await self.sleep(self.trend_time_frame.time)
+ continue
+
diff --git a/src/aiomql/lib/symbols/__init__.py b/src/aiomql/lib/symbols/__init__.py
new file mode 100644
index 0000000..423959b
--- /dev/null
+++ b/src/aiomql/lib/symbols/__init__.py
@@ -0,0 +1 @@
+from .forex_symbol import ForexSymbol
diff --git a/src/aiomql/lib/symbols/crypto_symbol.py b/src/aiomql/lib/symbols/crypto_symbol.py
new file mode 100644
index 0000000..39b7954
--- /dev/null
+++ b/src/aiomql/lib/symbols/crypto_symbol.py
@@ -0,0 +1,30 @@
+from ...symbol import Symbol
+from ...core.exceptions import VolumeError
+
+
+class CryptoSymbol(Symbol):
+ """Subclass of Symbol for Crypto/Fiat Symbols. Handles the computation of volume based on the amount to risk."""
+
+ async def compute_volume(self, *, amount: float, points, use_limits=False) -> float:
+ """Compute volume given an amount to risk and target pips. Round the computed volume to the nearest step.
+
+ Args:
+ amount (float): Amount to risk. Given in terms of the account currency.
+ points (float): Target pips.
+ use_limits (bool): If True, the computed volume checked against the maximum and minimum volume.
+
+ Returns:
+ float: volume
+
+ Raises:
+ VolumeError: If the computed volume is less than the minimum volume or greater than the maximum volume.
+ """
+ if self.currency_profit != self.account.currency:
+ amount = await self.convert_currency(amount=amount, base=self.currency_profit, quote=self.account.currency)
+ volume = amount / (self.point * points * self.trade_contract_size)
+ volume = self.round_off_volume(volume)
+ if self.check_volume(volume)[0]:
+ return volume
+ if use_limits:
+ return self.check_volume(volume)[1]
+ raise VolumeError(f'Incorrect Volume. Computed Volume outside the range of permitted volumes')
diff --git a/src/aiomql/lib/symbols/forex_symbol.py b/src/aiomql/lib/symbols/forex_symbol.py
new file mode 100644
index 0000000..029c67f
--- /dev/null
+++ b/src/aiomql/lib/symbols/forex_symbol.py
@@ -0,0 +1,32 @@
+from ...symbol import Symbol
+from ...core.exceptions import VolumeError
+
+
+class ForexSymbol(Symbol):
+ """Subclass of Symbol for Forex Symbols. Handles the conversion of currency and the computation of stop loss,
+ take profit and volume.
+ """
+
+ async def compute_volume(self, *, amount: float, pips, use_limits=False) -> float:
+ """Compute volume given an amount to risk and target pips. Round the computed volume to the nearest step.
+
+ Args:
+ amount (float): Amount to risk. Given in terms of the account currency.
+ pips (float): Target pips.
+ use_limits (bool): If True, the computed volume checked against the maximum and minimum volume.
+
+ Returns:
+ float: volume
+
+ Raises:
+ VolumeError: If the computed volume is less than the minimum volume or greater than the maximum volume.
+ """
+ if self.currency_profit != self.account.currency:
+ amount = await self.convert_currency(amount=amount, base=self.currency_profit, quote=self.account.currency)
+ volume = amount / (self.pip * pips * self.trade_contract_size)
+ volume = self.round_off_volume(volume)
+ if self.check_volume(volume)[0]:
+ return volume
+ if use_limits:
+ return self.check_volume(volume)[1]
+ raise VolumeError(f'Incorrect Volume. Computed Volume outside the range of permitted volumes')
diff --git a/src/aiomql/lib/traders/__init__.py b/src/aiomql/lib/traders/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/aiomql/ram.py b/src/aiomql/ram.py
index 45740e1..c87eca3 100644
--- a/src/aiomql/ram.py
+++ b/src/aiomql/ram.py
@@ -1,6 +1,5 @@
"""Risk Assessment and Management"""
from .account import Account
-from .symbol import Symbol
class RAM:
@@ -8,24 +7,20 @@ class RAM:
risk_to_reward: float
risk: float
amount: float
- pips: float
- volume: float
- def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, amount: float = 0, pips: float = 0, volume=0):
+ def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, amount: float = 0, **kwargs):
"""Initialize Risk Assessment and Management with the provided keyword arguments.
Keyword Args:
risk_to_reward (float): Risk to reward ratio. Defaults to 1
risk (float): Percentage of account balance to risk per trade 0.01 # 1%
amount (float): Amount to risk per trade in terms of account currency 0
- pips (float): Target pips to risk
- volume (float): Volume to trade 0
+ kwargs: extra keyword arguments are set as object attributes
"""
self.risk_to_reward = risk_to_reward
self.risk = risk
self.amount = amount
- self.pips = pips
- self.volume = volume
+ [setattr(self, key, value) for key, value in kwargs.items()]
async def get_amount(self, risk: float = 0) -> float:
"""Calculate the amount to risk per trade as a percentage of equity.
@@ -39,20 +34,3 @@ class RAM:
await self.account.refresh()
risk = risk or self.risk
return self.account.equity * risk
-
- async def get_volume(self, *, symbol: Symbol, pips: float = 0, amount: float = 0) -> float:
- """Calculate the volume to trade. if pips is not provided, the pips attribute is used.
- If the amount attribute or amount argument is zero, the amount is calculated using the get_amount method based
- on the risk.
-
- Keyword Args:
- symbol (Symbol): Financial instrument
- pips (float): Target pips. Defaults to zero.
- amount (float): Amount to risk per trade. Defaults to zero.
-
- Returns:
- float: Volume to trade
- """
- pips = pips or self.pips
- amount = amount or self.amount or await self.get_amount()
- return await symbol.compute_volume(amount=amount, pips=pips)
diff --git a/src/aiomql/sessions.py b/src/aiomql/sessions.py
index 3b57c2f..e1aeb31 100644
--- a/src/aiomql/sessions.py
+++ b/src/aiomql/sessions.py
@@ -19,12 +19,6 @@ def delta(obj: time):
return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
-def seconds(start: time, end: time) -> set[int]:
- if start > end:
- return set(range(delta(start).seconds, 86400)) | set(range(0, delta(end).seconds))
- return set(range(delta(start).seconds, delta(end).seconds))
-
-
class Session:
"""A session is a time period between two datetime.time objects specified in utc.
@@ -36,7 +30,6 @@ class Session:
custom_start (Callable): A custom function to call when the session starts. Default is None.
custom_end (Callable): A custom function to call when the session ends. Default is None.
name (str): A name for the session. Default is a combination of start and end.
- seconds (set[int]): A set of seconds between start and end.
Methods:
begin: Call the action specified in on_start or custom_start.
@@ -69,10 +62,13 @@ class Session:
self.custom_start = custom_start
self.custom_end = custom_end
self.name = name or f'{self.start} - {self.end}'
- self.seconds = seconds(self.start, self.end)
def __contains__(self, item: time):
- return delta(item).seconds in self.seconds
+ if self.start > self.end:
+ m1 = time(hour=23, minute=59, second=59, microsecond=9999)
+ m2 = time(hour=0)
+ return self.start <= item <= m1 or m2 <= item < self.end
+ return self.start <= item < self.end
def __str__(self):
return f'{self.start}-->{self.name}-->{self.end}' if self.name else f'{self.start}-->{self.end}'
diff --git a/src/aiomql/symbol.py b/src/aiomql/symbol.py
index fb2ef5e..7ba1b75 100644
--- a/src/aiomql/symbol.py
+++ b/src/aiomql/symbol.py
@@ -79,7 +79,7 @@ class Symbol(SymbolInfo):
Raises:
ValueError: If request was unsuccessful and None was returned
"""
-
+
info = await self.mt5.symbol_info(self.name)
if info:
self.set_attributes(**info._asdict())
@@ -96,6 +96,7 @@ class Symbol(SymbolInfo):
if await self.symbol_select():
await self.book_add()
await self.info()
+ await self.info_tick()
return True
logger.warning(f'Unable to initialized symbol {self}')
return False
@@ -137,6 +138,15 @@ class Symbol(SymbolInfo):
return await self.mt5.market_book_release(self.name)
def check_volume(self, volume) -> tuple[bool, float]:
+ """Check if the volume is within the limits of the symbol. If not, return the nearest limit.
+
+ Args:
+ volume (float): Volume to check
+
+ Returns: tuple[bool, float]: Returns a tuple of a boolean and a float. The boolean indicates if the volume is
+ within the limits of the symbol. The float is the volume to use if the volume is not within the limits of the
+ symbol.
+ """
check = self.volume_min <= volume <= self.volume_max
if check:
return check, volume
@@ -145,25 +155,36 @@ class Symbol(SymbolInfo):
else:
return check, self.volume_max
- def round_off_volume(self, volume):
+ def round_off_volume(self, volume) -> float:
+ """Round off the volume to the nearest volume step.
+
+ Args:
+ volume (float): Volume to round off
+
+ Returns:
+ float: Rounded off volume
+ """
step = ceil(abs(log10(self.volume_step)))
return round(volume, step)
- async def compute_volume(self, *, amount: float, pips: float, use_limits: bool = False) -> float:
- """Computes the volume of a trade based on the amount and the number of pips to target.
+ async def compute_volume(self, *args, **kwargs) -> float:
+ """Computes the volume required for a trade usually based on the amount and any other keyword arguments.
This is a dummy method that returns the minimum volume of the symbol. It is meant to be overridden by a subclass
that implements the computation of volume.
- Args:
- amount (float): Amount to risk in the trade
- pips (float): Number of pips to target
- use_limits (bool): If True, the computed volume is rounded to the nearest step and checked against
+ Keyword Args:
+ use_limits (bool): round up or round down the computed volume to the nearest volume limit i.e volume_min
+ or volume_max
Returns:
float: Returns the volume of the trade
"""
return self.volume_min
+ async def convert_currency(self, *, amount: float, base: str, quote: str) -> float:
+ """Convert from one currency to the other. Alias for currency_conversion"""
+ return await self.currency_conversion(amount=amount, base=base, quote=quote)
+
async def currency_conversion(self, *, amount: float, base: str, quote: str) -> float:
"""Convert from one currency to the other.
@@ -173,7 +194,7 @@ class Symbol(SymbolInfo):
quote: The quote currency of the pair
Returns:
- float: Amount in terms of the base currency or None if it failed to convert
+ float: Amount in terms of the base currency
Raises:
ValueError: If conversion is impossible
@@ -189,8 +210,7 @@ class Symbol(SymbolInfo):
if self.account.has_symbol(pair):
tick = await self.info_tick(name=pair)
if tick is not None:
- amount = amount * tick.bid
- return amount
+ return amount * tick.bid
except Exception as err:
logger.warning(f'Currency conversion failed: Unable to convert {amount} in {quote} to {base}')
raise ValueError(f'Currency Conversion Failed: {err}')
@@ -201,11 +221,11 @@ class Symbol(SymbolInfo):
"""
Get bars from the MetaTrader 5 terminal starting from the specified date.
- Args:
- timeframe (TimeFrame): Timeframe the bars are requested for. Set by a value from the TimeFrame enumeration. Required unnamed parameter.
+ Args: timeframe (TimeFrame): Timeframe the bars are requested for. Set by a value from the TimeFrame
+ enumeration. Required unnamed parameter.
- date_from (datetime | int): Date of opening of the first bar from the requested sample. Set by the 'datetime' object or as a number
- of seconds elapsed since 1970.01.01. Required unnamed parameter.
+ date_from (datetime | int): Date of opening of the first bar from the requested sample. Set by the
+ 'datetime' object or as a number of seconds elapsed since 1970.01.01. Required unnamed parameter.
count (int): Number of bars to receive. Required unnamed parameter.
@@ -220,7 +240,7 @@ class Symbol(SymbolInfo):
return Candles(data=rates)
raise ValueError(f'Could not get rates for {self.name}')
- async def copy_rates_from_pos(self, *,timeframe: TimeFrame, count: int = 500, start_position: int = 0) -> Candles:
+ async def copy_rates_from_pos(self, *, timeframe: TimeFrame, count: int = 500, start_position: int = 0) -> Candles:
"""Get bars from the MetaTrader 5 terminal starting from the specified index.
Args:
@@ -267,7 +287,8 @@ class Symbol(SymbolInfo):
return Candles(data=rates)
raise ValueError(f'Could not get rates for {self.name}')
- async def copy_ticks_from(self, *, date_from: datetime | int, count: int = 100, flags: CopyTicks = CopyTicks.ALL) -> Ticks:
+ async def copy_ticks_from(self, *, date_from: datetime | int, count: int = 100,
+ flags: CopyTicks = CopyTicks.ALL) -> Ticks:
"""
Get ticks from the MetaTrader 5 terminal starting from the specified date.
@@ -289,7 +310,8 @@ class Symbol(SymbolInfo):
return Ticks(data=ticks)
raise ValueError(f'Could not get ticks for {self.name}')
- async def copy_ticks_range(self, *, date_from: datetime | int, date_to: datetime | int, flags: CopyTicks = CopyTicks.ALL) -> Ticks:
+ async def copy_ticks_range(self, *, date_from: datetime | int, date_to: datetime | int,
+ flags: CopyTicks = CopyTicks.ALL) -> Ticks:
"""Get ticks for the specified date range from the MetaTrader 5 terminal.
Args:
@@ -298,7 +320,7 @@ class Symbol(SymbolInfo):
date_to: Date, up to which the bars are requested. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. Bars
with the open time <= date_to are returned. Required unnamed parameter.
-
+
flags (CopyTicks):
Returns:
diff --git a/src/aiomql/trader.py b/src/aiomql/trader.py
index 9e45c58..a98d107 100644
--- a/src/aiomql/trader.py
+++ b/src/aiomql/trader.py
@@ -8,7 +8,7 @@ from zoneinfo import ZoneInfo
from .order import Order
from .symbol import Symbol as _Symbol
from .ram import RAM
-from .core.models import OrderType
+from .core.models import OrderType, OrderSendResult
from .core.config import Config
from .utils import dict_to_string
from .result import Result
@@ -43,34 +43,43 @@ class Trader:
self.symbol = symbol
self.order = Order(symbol=symbol.name)
self.ram = ram or RAM()
+ self.params = {}
async def create_order(self, *, order_type: OrderType, **kwargs):
"""Complete the order object with the required values. Creates a simple order.
- Uses the ram instance to set the volume.
Args:
order_type (OrderType): Type of order
kwargs: keyword arguments as required for the specific trader
"""
- # check if pips is passed in as a keyword argument, if not use the pips attribute of the ram instance
- pips = kwargs.get('pips', 0) or self.ram.pips
- self.order.volume = kwargs.get('volume', self.ram.volume) or await self.ram.get_volume(symbol=self.symbol,
- pips=pips)
+ points = kwargs.get('points', self.symbol.trade_stops_level+self.symbol.spread)
+ self.order.volume = await self.symbol.compute_volume()
self.order.type = order_type
- await self.set_order_limits(pips=pips)
+ await self.set_trade_stop_levels(points=points)
async def set_order_limits(self, pips: float):
- """Sets the stop loss and take profit for the order.
- This method uses pips as defined for forex instruments.
+ """Sets the stop loss and take profit for the order. This method uses pips as defined for forex instruments.
Args:
pips: Target pips
"""
- # use passed in pips and the pip value of the symbol to calculate the stop loss and take profit.
- # this is sure to work for forex instruments.
pips = pips * self.symbol.pip
sl, tp = pips, pips * self.ram.risk_to_reward
tick = await self.symbol.info_tick()
+ if self.order.type == OrderType.BUY:
+ self.order.sl, self.order.tp = tick.ask - sl, tick.ask + tp
+ self.order.price = tick.ask
+ elif self.order.type == OrderType.SELL:
+ self.order.sl, self.order.tp = tick.bid + sl, tick.bid - tp
+ self.order.price = tick.bid
+ else:
+ raise ValueError(f"Invalid order type: {self.order.type}")
+
+ async def set_trade_stop_levels(self, *, points):
+ """Set the stop loss and take profit levels of the order based on the points."""
+ points = points * self.symbol.point
+ sl, tp = points, points * self.ram.risk_to_reward
+ tick = await self.symbol.info_tick()
if self.order.type == OrderType.BUY:
self.order.sl, self.order.tp = tick.ask - sl, tick.ask + tp
self.order.price = tick.ask
@@ -78,6 +87,46 @@ class Trader:
self.order.sl, self.order.tp = tick.bid + sl, tick.bid - tp
self.order.price = tick.bid
+ async def check_order(self) -> bool:
+ """Check order before sending it to the broker.
+
+ Returns:
+ bool: True if order can go through else false
+ """
+ check = await self.order.check()
+ if check.retcode != 0:
+ logger.warning(
+ f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(check.get_dict(include={'comment', 'retcode'}), multi=True)}")
+ return False
+ return True
+
+ async def send_order(self):
+ result = await self.order.send()
+ if result.retcode != 10009:
+ logger.warning(
+ f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(result.get_dict(include={'comment', 'retcode'}), multi=True)}")
+ return
+ logger.info(f"Symbol: {self.order.symbol}\nOrder: {dict_to_string(result.dict, multi=True)}\n")
+ await self.record_trade(result)
+
+ async def record_trade(self, result: OrderSendResult):
+ """
+ Record the trade in a csv file.
+ Args:
+ result (OrderSendResult): Result of the order send
+ """
+ if result.retcode != 10009 or not self.config.record_trades:
+ return
+ profit = await self.order.calc_profit()
+ params = self.params
+ params['expected_profit'] = profit
+ date = datetime.utcnow()
+ date = date.replace(tzinfo=ZoneInfo('UTC'))
+ params['date'] = date
+ params['time'] = date.timestamp()
+ res = Result(result=result, parameters=params)
+ await res.save_csv()
+
async def place_trade(self, order_type: OrderType, params: dict = None, **kwargs):
"""Places a trade based on the order_type.
@@ -88,35 +137,9 @@ class Trader:
"""
try:
await self.create_order(order_type=order_type, **kwargs)
-
- # Check the order before placing it
- check = await self.order.check()
- if check.retcode != 0:
- logger.warning(
- f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(check.get_dict(include={'comment', 'retcode'}), multi=True)}")
+ if not await self.check_order():
return
-
- # check expected profit
- profit = await self.order.calc_profit()
-
- # Send the order.
- result = await self.order.send()
- if result.retcode != 10009:
- logger.warning(
- f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(result.get_dict(include={'comment', 'retcode'}), multi=True)}")
- return
-
- logger.info(f"Symbol: {self.order.symbol}\nOrder: {dict_to_string(result.dict, multi=True)}\n")
-
- # save trade result and passed in parameters
- if result.retcode == 10009 and self.config.record_trades:
- params = params or {}
- params['expected_profit'] = profit
- date = datetime.utcnow()
- date = date.replace(tzinfo=ZoneInfo('UTC'))
- params['date'] = date
- params['time'] = date.timestamp()
- res = Result(result=result, parameters=params)
- await res.save_csv()
+ self.params |= params or {}
+ await self.send_order()
except Exception as err:
logger.error(f"{err}. Symbol: {self.order.symbol}\n {self.__class__.__name__}.place_trade")
diff --git a/src/aiomql/utils.py b/src/aiomql/utils.py
index a8e1b3e..eb50558 100644
--- a/src/aiomql/utils.py
+++ b/src/aiomql/utils.py
@@ -1,4 +1,5 @@
"""Utility functions for aiomql."""
+
def dict_to_string(data: dict, multi=False) -> str:
"""Convert a dict to a string. Use for logging.
diff --git a/tests/test_constants.py b/tests/test_constants.py
index b07d8a2..32b82cd 100644
--- a/tests/test_constants.py
+++ b/tests/test_constants.py
@@ -2,6 +2,6 @@ from aiomql import TradeAction
class TestConstants:
-
def test_trade_action(self):
assert TradeAction.DEAL == 1
+