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 +