2021-09-25 11:41:14 +08:00
|
|
|
import logging
|
|
|
|
|
import logging.config
|
|
|
|
|
from dataclasses import dataclass
|
2021-10-25 19:11:47 +08:00
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
from typing import Any
|
2021-09-25 11:41:14 +08:00
|
|
|
|
|
|
|
|
import MetaTrader5 as mt5
|
|
|
|
|
import numpy as np
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
|
|
|
|
import config
|
|
|
|
|
|
|
|
|
|
logging.config.fileConfig("logging.conf")
|
|
|
|
|
|
2021-09-30 10:56:56 +08:00
|
|
|
logger = logging.getLogger("app" if config.DEBUG else "root")
|
2021-09-25 11:41:14 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class Client:
|
2021-10-25 19:11:47 +08:00
|
|
|
__slots__ = [
|
|
|
|
|
"account",
|
|
|
|
|
"password",
|
|
|
|
|
"server",
|
|
|
|
|
"pair",
|
|
|
|
|
"lot",
|
|
|
|
|
"timeframe",
|
|
|
|
|
"y_predict",
|
|
|
|
|
"stop_loss",
|
|
|
|
|
]
|
2021-09-25 11:41:14 +08:00
|
|
|
account: int
|
|
|
|
|
password: str
|
|
|
|
|
server: str
|
|
|
|
|
pair: str
|
|
|
|
|
lot: float
|
|
|
|
|
timeframe: np.ndarray
|
|
|
|
|
|
2021-09-30 10:56:56 +08:00
|
|
|
def login(self) -> bool:
|
2021-09-25 11:41:14 +08:00
|
|
|
"Client login"
|
|
|
|
|
if mt5.login(
|
|
|
|
|
login=self.account, password=self.password, server=self.server
|
|
|
|
|
):
|
2021-09-30 10:56:56 +08:00
|
|
|
logger.debug(
|
2021-09-25 11:41:14 +08:00
|
|
|
f"Logged in to {self.server} with account {self.account}"
|
|
|
|
|
)
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
logger.critical(
|
|
|
|
|
f"Failed to login to {self.server} with account {self.account}"
|
|
|
|
|
)
|
|
|
|
|
return False
|
|
|
|
|
|
2021-10-13 20:15:37 +08:00
|
|
|
def determine_signal(
|
|
|
|
|
self, open_price: float, predicted_price: float
|
2021-10-25 19:11:47 +08:00
|
|
|
) -> Any:
|
2021-10-13 20:15:37 +08:00
|
|
|
"""Determine the trading signal based on which price is higher or lower"""
|
|
|
|
|
if open_price > predicted_price:
|
2021-09-25 11:41:14 +08:00
|
|
|
logger.debug(
|
2021-10-13 20:15:37 +08:00
|
|
|
f"Sell at {open_price} with predicted take profit {predicted_price}"
|
2021-09-25 11:41:14 +08:00
|
|
|
)
|
2021-09-30 10:56:56 +08:00
|
|
|
self.stop_loss = self.get_stop_loss(
|
2021-10-13 20:15:37 +08:00
|
|
|
mt5.symbol_info_tick(self.pair).bid, predicted_price
|
2021-09-30 10:56:56 +08:00
|
|
|
)
|
2021-10-25 19:11:47 +08:00
|
|
|
return self.order("sell")
|
2021-10-13 20:15:37 +08:00
|
|
|
elif open_price < predicted_price:
|
2021-09-25 11:41:14 +08:00
|
|
|
logger.debug(
|
2021-10-13 20:15:37 +08:00
|
|
|
f"Buy at {open_price} with predicted take profit {predicted_price}"
|
2021-09-25 11:41:14 +08:00
|
|
|
)
|
2021-09-30 10:56:56 +08:00
|
|
|
self.stop_loss = self.get_stop_loss(
|
2021-10-13 20:15:37 +08:00
|
|
|
mt5.symbol_info_tick(self.pair).ask, predicted_price
|
2021-09-30 10:56:56 +08:00
|
|
|
)
|
2021-10-25 19:11:47 +08:00
|
|
|
return self.order("buy")
|
2021-09-30 10:56:56 +08:00
|
|
|
else:
|
|
|
|
|
logger.warn(
|
|
|
|
|
"Miraculously the open price and the predicted price are exactly the same. Not sure what to do"
|
|
|
|
|
)
|
2021-10-25 19:11:47 +08:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def build_dataset(
|
|
|
|
|
self, df_data: np.ndarray, lookup_data: int = 1
|
|
|
|
|
) -> tuple[Any, Any]:
|
|
|
|
|
"""Split a dataframe and split the dataset to train a model"""
|
|
|
|
|
from sklearn.model_selection import train_test_split
|
2021-09-30 10:56:56 +08:00
|
|
|
|
2021-10-25 19:11:47 +08:00
|
|
|
df_data["adjclose"] = df_data["close"].shift(
|
|
|
|
|
periods=-lookup_data, fill_value=0
|
|
|
|
|
)
|
|
|
|
|
df_x = df_data.drop(columns="adjclose")
|
|
|
|
|
|
|
|
|
|
x_train, _xtest, y_train, _ytest = train_test_split(
|
|
|
|
|
df_x,
|
|
|
|
|
df_data["adjclose"],
|
|
|
|
|
test_size=0.2,
|
|
|
|
|
random_state=42,
|
|
|
|
|
shuffle=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return x_train, y_train
|
|
|
|
|
|
|
|
|
|
def analyze_and_trade(
|
|
|
|
|
self, rates: np.ndarray = None, current_rates: np.ndarray = None
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Analyze the past X rates and trade on the current open price. X rates is configured by BARS_TO_TRAIN."""
|
2021-10-13 20:15:37 +08:00
|
|
|
while rates is None:
|
|
|
|
|
rates = mt5.copy_rates_from_pos(
|
|
|
|
|
self.pair, self.timeframe, 1, config.BARS_TO_TRAIN
|
|
|
|
|
)
|
2021-10-25 19:11:47 +08:00
|
|
|
|
|
|
|
|
while current_rates is None:
|
|
|
|
|
current_rates = mt5.copy_rates_from_pos(
|
|
|
|
|
self.pair, self.timeframe, 0, 1
|
|
|
|
|
)
|
2021-10-13 20:15:37 +08:00
|
|
|
|
|
|
|
|
x_train, y_train = self.build_dataset(pd.DataFrame(rates))
|
|
|
|
|
model = self.get_model_to_predict(x_train, y_train)
|
2021-10-25 19:11:47 +08:00
|
|
|
current_open_price, to_predict = self.get_current_rate_to_predict(
|
|
|
|
|
current_rates
|
|
|
|
|
)
|
2021-10-13 20:15:37 +08:00
|
|
|
self.y_predict = model.predict(to_predict)
|
2021-10-25 19:11:47 +08:00
|
|
|
return self.determine_signal(
|
|
|
|
|
current_open_price, float(self.y_predict[0])
|
|
|
|
|
)
|
2021-10-13 20:15:37 +08:00
|
|
|
|
2021-09-30 10:56:56 +08:00
|
|
|
def get_stop_loss(
|
|
|
|
|
self, open_price: float, predicted_price: float
|
|
|
|
|
) -> float:
|
|
|
|
|
if open_price > predicted_price:
|
2021-10-25 19:11:47 +08:00
|
|
|
return open_price + ((open_price - predicted_price))
|
2021-09-30 10:56:56 +08:00
|
|
|
else:
|
2021-10-25 19:11:47 +08:00
|
|
|
return open_price - ((predicted_price - open_price))
|
2021-09-25 11:41:14 +08:00
|
|
|
|
2021-10-25 19:11:47 +08:00
|
|
|
def get_current_rate_to_predict(self, current_rate: np.ndarray = None):
|
2021-09-25 11:41:14 +08:00
|
|
|
"""
|
|
|
|
|
Get the current rate, and open price
|
|
|
|
|
"""
|
|
|
|
|
current_df = pd.DataFrame(current_rate)
|
|
|
|
|
current_open_price = current_df.iloc[0]["open"]
|
2021-10-25 19:11:47 +08:00
|
|
|
return current_open_price, current_df
|
2021-09-25 11:41:14 +08:00
|
|
|
|
|
|
|
|
def get_model_to_predict(self, x_train, y_train):
|
|
|
|
|
"""
|
|
|
|
|
Build a model to fit with the x and y training data set and fit.
|
|
|
|
|
|
|
|
|
|
Returns the built and learned model
|
2021-09-30 10:56:56 +08:00
|
|
|
|
|
|
|
|
TODO: Investigate and find out what's a better model if not better from scikit-learn's Linear Regression.
|
2021-09-25 11:41:14 +08:00
|
|
|
"""
|
|
|
|
|
from sklearn.linear_model import LinearRegression
|
|
|
|
|
|
|
|
|
|
return LinearRegression().fit(x_train, y_train)
|
|
|
|
|
|
2021-10-25 19:11:47 +08:00
|
|
|
def set_lot_size(self) -> float:
|
|
|
|
|
"""Get from History Orders. If the last History Order was a loss, double the lot size in the next trade."""
|
|
|
|
|
to_date = datetime.now()
|
|
|
|
|
from_date = to_date - timedelta(days=4)
|
2021-10-26 22:09:00 +08:00
|
|
|
logger.debug(f"!!!CHECKING HISTORY DEALS FOR {self.pair}!!!")
|
2021-10-25 19:11:47 +08:00
|
|
|
history_deals = mt5.history_deals_get(
|
|
|
|
|
from_date, to_date, group=self.pair
|
|
|
|
|
)
|
|
|
|
|
if history_deals is None:
|
|
|
|
|
logger.debug(
|
|
|
|
|
f"No history orders with this pair: {self.pair}. Error code = {mt5.last_error()}"
|
|
|
|
|
)
|
|
|
|
|
return self.lot
|
|
|
|
|
elif len(history_deals) > 0:
|
|
|
|
|
comment = history_deals[len(history_deals) - 1].comment
|
|
|
|
|
if "tp" in comment or "sl" not in comment:
|
|
|
|
|
return self.lot
|
|
|
|
|
traded_volume = history_deals[len(history_deals) - 1].volume
|
|
|
|
|
return traded_volume + traded_volume
|
|
|
|
|
else:
|
|
|
|
|
logger.debug(f"Something went wrong. Error: {mt5.last_error()}")
|
|
|
|
|
return self.lot
|
|
|
|
|
|
|
|
|
|
def order(self, signal: str) -> bool:
|
2021-09-30 10:56:56 +08:00
|
|
|
"""Depending on the param signal, this will place an order for BUY or SELL"""
|
2021-09-25 11:41:14 +08:00
|
|
|
symbol_info = mt5.symbol_info(self.pair)
|
|
|
|
|
if symbol_info is None:
|
|
|
|
|
logger.error(f"{self.pair} not found, cannot call order_check()")
|
|
|
|
|
mt5.shutdown()
|
|
|
|
|
quit()
|
|
|
|
|
|
|
|
|
|
if not symbol_info.visible:
|
|
|
|
|
logger.debug(f"{self.pair} is not visible, trying to switch on")
|
|
|
|
|
if not mt5.symbol_select(self.pair, True):
|
|
|
|
|
logger.error(f"symbol_select({self.pair}) failed, exit")
|
|
|
|
|
mt5.shutdown()
|
|
|
|
|
quit()
|
2021-09-30 10:56:56 +08:00
|
|
|
symbol_info_tick = mt5.symbol_info_tick(self.pair)
|
2021-09-25 11:41:14 +08:00
|
|
|
order_type, price = (
|
2021-09-30 10:56:56 +08:00
|
|
|
(mt5.ORDER_TYPE_SELL, symbol_info_tick.bid)
|
2021-09-25 11:41:14 +08:00
|
|
|
if signal == "sell"
|
2021-09-30 10:56:56 +08:00
|
|
|
else (mt5.ORDER_TYPE_BUY, symbol_info_tick.ask)
|
2021-09-25 11:41:14 +08:00
|
|
|
)
|
2021-10-25 19:11:47 +08:00
|
|
|
self.lot = self.set_lot_size()
|
2021-09-25 11:41:14 +08:00
|
|
|
self.raw_order(
|
|
|
|
|
action=mt5.TRADE_ACTION_DEAL,
|
|
|
|
|
symbol=self.pair,
|
|
|
|
|
volume=self.lot,
|
|
|
|
|
type=order_type,
|
|
|
|
|
price=price,
|
|
|
|
|
tp=float(self.y_predict[0]),
|
2021-10-25 19:11:47 +08:00
|
|
|
sl=float(self.stop_loss),
|
2021-09-25 11:41:14 +08:00
|
|
|
deviation=20,
|
2021-09-30 10:56:56 +08:00
|
|
|
magic=20210927,
|
|
|
|
|
comment=config.COMMENT,
|
2021-09-25 11:41:14 +08:00
|
|
|
type_time=mt5.ORDER_TIME_DAY,
|
|
|
|
|
type_filling=mt5.ORDER_FILLING_IOC,
|
|
|
|
|
)
|
|
|
|
|
logger.debug(
|
|
|
|
|
f"Order Sent : by {self.pair} {self.lot} lots at {price} with deviation 20 points"
|
|
|
|
|
)
|
2021-10-25 19:11:47 +08:00
|
|
|
return True
|
2021-09-25 11:41:14 +08:00
|
|
|
|
|
|
|
|
def raw_order(self, **kwargs):
|
2021-09-30 10:56:56 +08:00
|
|
|
"""Sends the order from kwargs returns the result dictionary"""
|
2021-09-25 11:41:14 +08:00
|
|
|
logger.debug(kwargs)
|
2021-09-30 10:56:56 +08:00
|
|
|
result = (
|
|
|
|
|
mt5.order_check(kwargs) if config.DEBUG else mt5.order_send(kwargs)
|
|
|
|
|
)
|
2021-09-25 11:41:14 +08:00
|
|
|
if result.retcode != mt5.TRADE_RETCODE_DONE:
|
2021-09-30 10:56:56 +08:00
|
|
|
logger.error(
|
|
|
|
|
f"Order Send Failed for {self.pair}, RETCODE = {result.retcode}"
|
|
|
|
|
)
|
2021-09-25 11:41:14 +08:00
|
|
|
else:
|
2021-09-30 10:56:56 +08:00
|
|
|
logger.info(f"Order send done. Result: {result}")
|
2021-10-25 19:11:47 +08:00
|
|
|
logger.debug(f"Result Type: {type(result)}")
|
2021-09-25 11:41:14 +08:00
|
|
|
return result
|
|
|
|
|
|
2021-09-30 10:56:56 +08:00
|
|
|
def check_existing_positions(self) -> bool:
|
2021-09-25 11:41:14 +08:00
|
|
|
"""
|
|
|
|
|
Returns if there's any existing positions.
|
|
|
|
|
This is to help keep a limit to the number of orders a specific pair can handle.
|
|
|
|
|
In this function, its to handle that there can only be 1 existing active order per symbol/pair.
|
|
|
|
|
"""
|
|
|
|
|
logger.debug(f"Checking pair: {self.pair}")
|
|
|
|
|
pos = mt5.positions_get(symbol=self.pair)
|
|
|
|
|
logger.debug(f"{pos}")
|
2021-10-26 22:09:00 +08:00
|
|
|
return pos is None or len(pos) == config.NO_CONCURRENT_TRADES
|
2021-09-25 11:41:14 +08:00
|
|
|
|
|
|
|
|
|
2021-09-30 10:56:56 +08:00
|
|
|
def main() -> None:
|
|
|
|
|
"""Start the bot"""
|
2021-09-25 11:41:14 +08:00
|
|
|
if not mt5.initialize():
|
|
|
|
|
logger.critical(f"MT5 Init failed, error code {mt5.last_error()}")
|
|
|
|
|
quit()
|
|
|
|
|
else:
|
|
|
|
|
for p in config.PAIRS:
|
|
|
|
|
c = Client(
|
|
|
|
|
config.ACCOUNT,
|
|
|
|
|
config.PASSWORD,
|
|
|
|
|
config.SERVER,
|
|
|
|
|
p,
|
|
|
|
|
config.LOT_SIZE,
|
|
|
|
|
config.TIMEFRAME,
|
|
|
|
|
)
|
|
|
|
|
if c.login() and c.check_existing_positions():
|
2021-10-25 19:11:47 +08:00
|
|
|
c.analyze_and_trade()
|
2021-09-25 11:41:14 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2021-09-25 15:59:30 +08:00
|
|
|
try:
|
|
|
|
|
main()
|
|
|
|
|
except (KeyboardInterrupt, SystemExit):
|
|
|
|
|
logger.critical("Manually quit the program")
|