Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions tradelocker_bot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# DarkGPT TradeLocker Bot

Automated trading bot for **TradeLocker** (Atlas Funded Accounts).

## Strategy

| Component | Detail |
|-----------|--------|
| Signal | EMA(9) crosses EMA(21) |
| Confirmation | RSI(14) momentum filter |
| Stop-Loss | 1.5 × ATR(14) |
| Take-Profit | 2.5 × ATR(14) — R:R ≈ 1.67 |
| Timeframe | 15-minute bars |
| Sessions | London (07-12 UTC) + New York (13-17 UTC) |

## Atlas Risk Rules Built-In

- Max **0.5% risk per trade** (configurable)
- Max **2% daily loss** — bot stops for the day if hit
- Max **4% total drawdown** — bot stops if hit
- Max **2 concurrent positions**

## Setup

```bash
pip install -r requirements.txt
```

Edit `config.py`:

```python
TRADELOCKER_BASE_URL = "https://live.tradelocker.com" # live account
API_EMAIL = "you@email.com"
API_PASSWORD = "yourpassword"
API_SERVER = "YOUR-BROKER-SERVER"
```

## Run

```bash
python bot.py
```

Logs are written to `bot.log` and stdout.

## Files

```
bot.py — main loop
config.py — all settings in one place
tradelocker_api.py — TradeLocker REST wrapper (auth, orders, candles)
indicators.py — EMA, RSI, ATR, signal logic
risk_manager.py — position sizing, daily/total drawdown guards
session_filter.py — London/NY session gate
```

## Tips for Atlas

- Keep `RISK_PER_TRADE_PCT` at **0.5 %** or lower
- Never widen the `MAX_DAILY_LOSS_PCT` beyond your Atlas daily limit
- Use the **demo** URL first to paper-trade and verify signals
- Monitor `bot.log` daily
143 changes: 143 additions & 0 deletions tradelocker_bot/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""
DarkGPT TradeLocker Bot — Main loop
Atlas Funded Account | EMA crossover + RSI + ATR

Run:
python bot.py
"""

import time
import logging
import sys

import config
from tradelocker_api import TradeLockerAPI
from indicators import compute_signal
from risk_manager import RiskManager
from session_filter import in_trading_session

# ─── Logging ─────────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-7s %(message)s",
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler("bot.log"),
],
)
log = logging.getLogger(__name__)


# ─── State ────────────────────────────────────────────────────────────────────
# Track which symbols we already have an open position in (avoid re-entry)
open_symbol_ids: set[str] = set()


def run():
api = TradeLockerAPI()
risk = RiskManager()

log.info("=== DarkGPT TradeLocker Bot starting ===")
api.login()

# Pre-resolve instrument ids once
instrument_map: dict[str, str] = {}
for sym in config.SYMBOLS:
iid = api.get_instrument_id(sym)
if iid:
instrument_map[sym] = iid
log.info("Resolved %s → id=%s", sym, iid)
else:
log.warning("Could not resolve %s — skipping.", sym)

if not instrument_map:
log.error("No valid instruments. Check your symbol list and account server.")
return

# Initialise risk manager with current balance
bal_data = api.get_balance()
balance = float(bal_data.get("balance", bal_data.get("equity", 10000)))
risk.initialise(balance)

log.info("Starting main loop. Polling every %ds …", config.POLL_INTERVAL_SECONDS)

while True:
try:
_cycle(api, risk, instrument_map)
except KeyboardInterrupt:
log.info("Bot stopped by user.")
break
except Exception as exc:
log.error("Cycle error: %s", exc, exc_info=True)

time.sleep(config.POLL_INTERVAL_SECONDS)


def _cycle(api: TradeLockerAPI, risk: RiskManager, instrument_map: dict[str, str]):
"""One polling cycle: fetch data → evaluate → act."""

# ── Get current state ────────────────────────────────────────────────────
bal_data = api.get_balance()
balance = float(bal_data.get("balance", bal_data.get("equity", 0)))
open_positions = api.get_open_positions()
open_count = len(open_positions)

# Update the set of instruments that already have a position
open_inst_ids = {str(p.get("tradableInstrumentId", "")) for p in open_positions}

# ── Session guard ────────────────────────────────────────────────────────
if not in_trading_session():
log.debug("Outside trading session — skipping.")
return

# ── Risk guard ───────────────────────────────────────────────────────────
allowed, reason = risk.can_trade(balance, open_count)
if not allowed:
log.warning("Trading paused: %s", reason)
return

# ── Evaluate each symbol ─────────────────────────────────────────────────
for sym, iid in instrument_map.items():
if iid in open_inst_ids:
log.debug("%s: already in a position — skip.", sym)
continue

candles = api.get_candles(iid, config.TIMEFRAME, config.CANDLES_NEEDED)
if not candles:
log.warning("%s: no candle data.", sym)
continue

sig = compute_signal(candles, config)

if sig["signal"] == "none":
log.debug("%s: no signal.", sym)
continue

# ── Size position ────────────────────────────────────────────────────
price = candles[-1]["close"]
sl_dist = abs(price - sig["sl"])
lot = risk.calculate_lot_size(balance, sl_dist)

side = "buy" if sig["signal"] == "long" else "sell"

log.info(
"SIGNAL %s %s @%.5f SL=%.5f TP=%.5f lot=%.2f",
sym, side.upper(), price, sig["sl"], sig["tp"], lot,
)

# ── Place order ──────────────────────────────────────────────────────
try:
api.place_market_order(
instrument_id=iid,
side=side,
qty=lot,
sl=sig["sl"],
tp=sig["tp"],
)
open_inst_ids.add(iid) # prevent double-entry this cycle
except Exception as exc:
log.error("Order failed for %s: %s", sym, exc)


if __name__ == "__main__":
run()
45 changes: 45 additions & 0 deletions tradelocker_bot/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
TradeLocker Bot Configuration
Atlas Funded Account - Conservative high win-rate settings
"""

# ─── TradeLocker API ──────────────────────────────────────────────────────────
TRADELOCKER_BASE_URL = "https://demo.tradelocker.com" # change to live: https://live.tradelocker.com
API_EMAIL = "your_email@example.com"
API_PASSWORD = "your_password"
API_SERVER = "OSP-DEMO" # your broker server name

# ─── Account / Risk ───────────────────────────────────────────────────────────
ACCOUNT_ID = None # auto-fetched on login
RISK_PER_TRADE_PCT = 0.5 # 0.5 % of balance per trade (Atlas safe)
MAX_DAILY_LOSS_PCT = 2.0 # hard stop for the day (Atlas rule)
MAX_TOTAL_DRAWDOWN_PCT = 4.0 # absolute drawdown ceiling (Atlas rule)
MAX_OPEN_TRADES = 2 # never stack more than 2 positions

# ─── Instruments to trade ─────────────────────────────────────────────────────
# Stick to the most liquid, tightest-spread pairs for best execution
SYMBOLS = ["EURUSD", "GBPUSD", "USDJPY"]

# ─── Timeframe ────────────────────────────────────────────────────────────────
TIMEFRAME = "15" # 15-minute bars (best signal/noise for intraday)
CANDLES_NEEDED = 100 # history bars to fetch per cycle

# ─── Strategy Parameters ──────────────────────────────────────────────────────
EMA_FAST = 9
EMA_SLOW = 21
RSI_PERIOD = 14
RSI_LONG = 55 # RSI must be > this to take a long (momentum filter)
RSI_SHORT = 45 # RSI must be < this to take a short
ATR_PERIOD = 14
ATR_SL_MULT = 1.5 # stop-loss = ATR * this
ATR_TP_MULT = 2.5 # take-profit = ATR * this (R:R ≈ 1.67)

# ─── Session Filter ───────────────────────────────────────────────────────────
# Only trade during the two most liquid windows (UTC)
TRADE_SESSIONS = [
(7, 12), # London open
(13, 17), # New York open
]

# ─── Loop timing ─────────────────────────────────────────────────────────────
POLL_INTERVAL_SECONDS = 30 # check every 30 s
128 changes: 128 additions & 0 deletions tradelocker_bot/indicators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""
Pure-Python technical indicators (no external libs needed).
All functions accept a list of floats and return a float or list.
"""

from typing import Optional


# ─── EMA ──────────────────────────────────────────────────────────────────────

def ema(values: list[float], period: int) -> list[float]:
"""Exponential Moving Average — same as most platforms."""
if len(values) < period:
return []
k = 2.0 / (period + 1)
out = [sum(values[:period]) / period] # seed with SMA
for v in values[period:]:
out.append(v * k + out[-1] * (1 - k))
return out


def last_ema(values: list[float], period: int) -> Optional[float]:
e = ema(values, period)
return e[-1] if e else None


# ─── RSI ──────────────────────────────────────────────────────────────────────

def rsi(values: list[float], period: int = 14) -> Optional[float]:
"""Wilder RSI — returns the latest value or None if not enough data."""
if len(values) < period + 1:
return None
gains, losses = [], []
for i in range(1, len(values)):
d = values[i] - values[i - 1]
gains.append(max(d, 0))
losses.append(max(-d, 0))
# Wilder smoothing
avg_g = sum(gains[:period]) / period
avg_l = sum(losses[:period]) / period
for i in range(period, len(gains)):
avg_g = (avg_g * (period - 1) + gains[i]) / period
avg_l = (avg_l * (period - 1) + losses[i]) / period
if avg_l == 0:
return 100.0
rs = avg_g / avg_l
return round(100 - 100 / (1 + rs), 2)


# ─── ATR ──────────────────────────────────────────────────────────────────────

def atr(highs: list[float], lows: list[float], closes: list[float], period: int = 14) -> Optional[float]:
"""Average True Range (Wilder smoothing)."""
if len(highs) < period + 1:
return None
trs = []
for i in range(1, len(highs)):
tr = max(
highs[i] - lows[i],
abs(highs[i] - closes[i - 1]),
abs(lows[i] - closes[i - 1]),
)
trs.append(tr)
avg = sum(trs[:period]) / period
for t in trs[period:]:
avg = (avg * (period - 1) + t) / period
return avg


# ─── Signal generation ────────────────────────────────────────────────────────

def compute_signal(candles: list[dict], cfg) -> dict:
"""
Returns:
{
'signal': 'long' | 'short' | 'none',
'sl': float,
'tp': float,
'atr': float,
}
"""
if len(candles) < max(cfg.EMA_SLOW, cfg.RSI_PERIOD + 1, cfg.ATR_PERIOD + 1) + 5:
return {"signal": "none"}

closes = [c["close"] for c in candles]
highs = [c["high"] for c in candles]
lows = [c["low"] for c in candles]

ema_f = ema(closes, cfg.EMA_FAST)
ema_s = ema(closes, cfg.EMA_SLOW)

if len(ema_f) < 2 or len(ema_s) < 2:
return {"signal": "none"}

cur_f, cur_s = ema_f[-1], ema_s[-1]
prev_f, prev_s = ema_f[-2], ema_s[-2]

cur_rsi = rsi(closes, cfg.RSI_PERIOD)
if cur_rsi is None:
return {"signal": "none"}

cur_atr = atr(highs, lows, closes, cfg.ATR_PERIOD)
if cur_atr is None:
return {"signal": "none"}

price = closes[-1]
sl_dist = cur_atr * cfg.ATR_SL_MULT
tp_dist = cur_atr * cfg.ATR_TP_MULT

# Bullish crossover: fast crosses above slow + RSI confirms momentum
if prev_f <= prev_s and cur_f > cur_s and cur_rsi > cfg.RSI_LONG:
return {
"signal": "long",
"sl": round(price - sl_dist, 5),
"tp": round(price + tp_dist, 5),
"atr": cur_atr,
}

# Bearish crossover: fast crosses below slow + RSI confirms
if prev_f >= prev_s and cur_f < cur_s and cur_rsi < cfg.RSI_SHORT:
return {
"signal": "short",
"sl": round(price + sl_dist, 5),
"tp": round(price - tp_dist, 5),
"atr": cur_atr,
}

return {"signal": "none"}
1 change: 1 addition & 0 deletions tradelocker_bot/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests>=2.31.0
Loading