98 lines
4.1 KiB
Python
98 lines
4.1 KiB
Python
import time, yaml
| |||
from core.connector import MT5
| |||
from risk.manager import RiskManager
| |||
from filters.filters import Filters
| |||
from strategies import REGISTRY
| |||
from notifier.telegram import Telegram, BotState
| |||
from utils.indicators import atr
| |||
from utils.logger import get_logger
| |||
| |||
log = get_logger()
| |||
| |||
def main():
| |||
cfg = yaml.safe_load(open("config.yaml", encoding="utf-8"))
| |||
bot = cfg["bot"]
| |||
mt = MT5(cfg["account"], bot["magic"], bot["deviation"])
| |||
if not mt.connect():
| |||
raise SystemExit("Impossible de se connecter à MT5")
| |||
| |||
risk, filt, state = RiskManager(cfg["risk"]), Filters(cfg["filters"]), BotState()
| |||
| |||
def status():
| |||
a = mt.account()
| |||
pos = mt.positions()
| |||
pnl = sum(p.profit for p in pos)
| |||
return (f"Solde {a.balance:.2f} | Equity {a.equity:.2f}\n"
| |||
f"Positions: {len(pos)} | PnL flottant {pnl:.2f}\n"
| |||
f"{'⏸ PAUSE' if state.paused else '▶️ ACTIF'}"
| |||
f"{' | 🛑 ' + risk.halted_reason if risk.halted_reason else ''}")
| |||
| |||
tg = Telegram(cfg["telegram"], state, status)
| |||
strategies = []
| |||
for s in cfg["strategies"]:
| |||
for sym in s["symbols"]:
| |||
strategies.append((sym, REGISTRY[s["name"]](s["timeframe"], s["params"])))
| |||
last_bar = {}
| |||
tg.send("🚀 Bot démarré")
| |||
| |||
try:
| |||
while True:
| |||
if not mt.ensure():
| |||
time.sleep(10); continue
| |||
a = mt.account()
| |||
risk.update(a.balance, a.equity)
| |||
| |||
if state.close_all:
| |||
for p in mt.positions(): mt.close_position(p)
| |||
state.close_all, state.paused = False, True
| |||
tg.send("✅ Positions fermées, bot en pause")
| |||
| |||
ok, reason = risk.check(a.equity)
| |||
if not ok and not risk.halted_reason:
| |||
risk.halted_reason = reason
| |||
log.error(f"KILL-SWITCH: {reason}")
| |||
for p in mt.positions(): mt.close_position(p)
| |||
tg.send(f"🛑 KILL-SWITCH: {reason}\nPositions fermées jusqu'à demain.")
| |||
| |||
# --- gestion des positions ouvertes (break-even / trailing)
| |||
for p in mt.positions():
| |||
df = mt.rates(p.symbol, "M15", 100)
| |||
t = mt.tick(p.symbol)
| |||
if df is None or t is None: continue
| |||
price = t.bid if p.type == 0 else t.ask
| |||
sl = risk.new_sl(p, atr(df.iloc[:-1], 14).iloc[-1], price)
| |||
if sl: mt.modify_sl(p, sl)
| |||
| |||
# --- recherche de signaux
| |||
if not (state.paused or risk.halted_reason):
| |||
for sym, strat in strategies:
| |||
df = mt.rates(sym, strat.timeframe, 500)
| |||
if df is None or len(df) < 250: continue
| |||
df = df.iloc[:-1] # barres clôturées uniquement
| |||
key = (sym, strat.name)
| |||
if last_bar.get(key) == df.time.iloc[-1]: continue
| |||
last_bar[key] = df.time.iloc[-1]
| |||
| |||
sig = strat.on_bar(sym, df)
| |||
if not sig: continue
| |||
info, tick = mt.info(sym), mt.tick(sym)
| |||
good, why = filt.all_ok(sym, info, tick, df)
| |||
if not good:
| |||
log.info(f"Signal {sym} {strat.name} ignoré: {why}"); continue
| |||
if not risk.can_open(mt.positions(), mt.positions(sym)):
| |||
continue
| |||
lots = risk.lot_size(a.equity, sig.sl_distance, info)
| |||
if lots <= 0: continue
| |||
px = tick.ask if sig.side == "buy" else tick.bid
| |||
sl = px - sig.sl_distance if sig.side == "buy" else px + sig.sl_distance
| |||
tp = px + sig.tp_distance if sig.side == "buy" else px - sig.tp_distance
| |||
if mt.open_trade(sym, sig.side, lots, sl, tp, f"{strat.name}"):
| |||
tg.send(f"📈 {sig.side.upper()} {sym} {lots} lots | {strat.name}\nSL {sl:.5f} TP {tp:.5f}")
| |||
time.sleep(bot["poll_seconds"])
| |||
except KeyboardInterrupt:
| |||
log.info("Arrêt demandé")
| |||
finally:
| |||
mt.shutdown()
| |||
| |||
if __name__ == "__main__":
| |||
main()
|