改为自己的mt5api

This commit is contained in:
2026-07-11 02:42:55 +08:00
commit 0764a9a07f
48 changed files with 13282 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
__pycache__
*.pyc
*.pyo
.env
.git
*.md
LICENSE
setup.sh
install.sh
+66
View File
@@ -0,0 +1,66 @@
# GENESIS Trading System — Environment Template
# ─────────────────────────────────────────────────────────────────────────────
# Copy this to .env and fill in your values:
# cp .env.example .env
#
# Or run the interactive setup wizard (recommended):
# bash setup.sh
# ─────────────────────────────────────────────────────────────────────────────
# ── Mt5Bridge API ─────────────────────────────────────────────────────────────
# Your own Mt5Bridge REST API server.
# See: Mt5Bridge使用指南.md for setup instructions.
#
# MT5_BRIDGE_URL = Base URL of your Mt5Bridge server (e.g. http://127.0.0.1:5000)
# MT5_BRIDGE_TOKEN = API token for authentication (if required)
# MT5_BRIDGE_ACCOUNT = MT5 account number (login ID)
# MT5_SYMBOL_MAP = JSON symbol mapping, e.g. {"EURUSDxx":"EURUSD","XAUUSDxx":"XAUUSD"}
# If empty, "xx" suffix is auto-stripped (EURUSDxx → EURUSD)
MT5_BRIDGE_URL=http://127.0.0.1:5000
MT5_BRIDGE_TOKEN=
MT5_BRIDGE_ACCOUNT=
MT5_SYMBOL_MAP=
# ── Telegram ──────────────────────────────────────────────────────────────────
# Create a bot: https://t.me/BotFather → /newbot
# Get your Chat ID: https://t.me/userinfobot
TELEGRAM_BOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN
TELEGRAM_CHAT_ID=YOUR_TELEGRAM_CHAT_ID
# ── LLM API (Hermes Brain) — OpenAI-Compatible ────────────────────────────────
# Powers the hourly macro analysis by Hermes.
# Supports ANY OpenAI-compatible API provider.
#
# Provider examples:
# OpenAI: OPENAI_BASE_URL=https://api.openai.com/v1 HERMES_MODEL=gpt-4o-mini
# DeepSeek: OPENAI_BASE_URL=https://api.deepseek.com/v1 HERMES_MODEL=deepseek-chat
# Qwen (Ali): OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 HERMES_MODEL=qwen-plus
# Groq: OPENAI_BASE_URL=https://api.groq.com/openai/v1 HERMES_MODEL=llama-3.3-70b-versatile
# Together AI: OPENAI_BASE_URL=https://api.together.xyz/v1 HERMES_MODEL=meta-llama/Llama-3-70b-chat-hf
# SiliconFlow: OPENAI_BASE_URL=https://api.siliconflow.cn/v1 HERMES_MODEL=deepseek-ai/DeepSeek-V3
# Ollama local: OPENAI_BASE_URL=http://127.0.0.1:11434/v1 HERMES_MODEL=llama3
# OpenRouter: OPENAI_BASE_URL=https://openrouter.ai/api/v1 HERMES_MODEL=openai/gpt-4o-mini
#
# Note: Some providers may not support "response_format: json_object".
# If you get errors, try removing it or switching to a provider that does.
OPENAI_API_KEY=sk-your-api-key-here
OPENAI_BASE_URL=https://api.openai.com/v1
HERMES_MODEL=gpt-4o-mini
HERMES_JSON_FORMAT=true # Set to "false" if your provider doesn't support response_format: json_object
# ── Risk Limits ───────────────────────────────────────────────────────────────
# These are enforced by every strategy module before any trade is placed.
# Hermes will refuse any trade that violates these limits.
MAX_POSITIONS=4 # Maximum simultaneous open positions
MAX_RISK_PCT=0.02 # Maximum risk per trade (2% of balance)
MAX_LOTS=3.0 # Maximum lot size per single trade
# ── Optional: Twelve Data ─────────────────────────────────────────────────────
# For economic calendar enrichment (news filter)
# Free tier: https://twelvedata.com
TWELVE_DATA_API_KEY=
+30
View File
@@ -0,0 +1,30 @@
# Environment & secrets
.env
*.env
# Python
__pycache__/
*.py[cod]
*.pyo
.venv/
venv/
.venv-hermes/
*.egg-info/
dist/
build/
# Logs & runtime state
*.log
*.jsonl
grid_state.json
genesis_cache.json
/tmp/
# macOS
.DS_Store
.AppleDouble
# IDE
.idea/
.vscode/
*.swp
+900
View File
@@ -0,0 +1,900 @@
# GENESIS: Building a Fully Autonomous MT5 Trading System with Six Strategy Bots and a Python REST API
> **Published by API2TRADE** · [app.api2trade.com](https://app.api2trade.com)
> **Open-source repository:** `api2trade/Genesis-Metatrader-Automatic-AI-Trading-System` (GPL-3.0)
> **Status:** Live | Engine v2.1 | Tested on Deriv Demo ($10,000 USD) and Exness MT5
---
## Table of Contents
1. [What Was Actually Built](#1-what-was-actually-built)
2. [Why REST Instead of a Local MT5 Terminal](#2-why-rest-instead-of-a-local-mt5-terminal)
3. [Core Architecture — How All Parts Connect](#3-core-architecture--how-all-parts-connect)
4. [The API2TRADE Integration — Every Real Endpoint Used](#4-the-api2trade-integration--every-real-endpoint-used)
5. [The Six Strategy Gods — Real Implementation Deep Dive](#5-the-six-strategy-gods--real-implementation-deep-dive)
6. [Hermes — The LLM Orchestration Brain](#6-hermes--the-llm-orchestration-brain)
7. [Market Data — Multi-Source Fallback Architecture](#7-market-data--multi-source-fallback-architecture)
8. [Risk Management — The Hard Layer](#8-risk-management--the-hard-layer)
9. [How to Replicate This — Complete Step-by-Step](#9-how-to-replicate-this--complete-step-by-step)
10. [Infrastructure and Real Costs](#10-infrastructure-and-real-costs)
11. [Key Engineering Challenges and How We Solved Them](#11-key-engineering-challenges-and-how-we-solved-them)
12. [Live Test Results — Deriv Demo Account](#12-live-test-results--deriv-demo-account)
13. [What to Do Next](#13-what-to-do-next)
---
## 1. What Was Actually Built
GENESIS is **8,401 lines of Python** split across 44 files that form a complete autonomous trading engine. It runs six independent strategy bots on a cron schedule, managed by an LLM orchestrator named Hermes, all communicating directly with MetaTrader 5 via the [API2TRADE](https://app.api2trade.com) REST API.
This is not a demo, a tutorial skeleton, or a toy project. It is a system we built, debugged, deployed to production, tested on a live MT5 account, and then open-sourced so you can replicate it exactly.
**What the system does every 5 minutes:**
1. `genesis_autonomous.py` fires via cron
2. It checks the live MT5 account balance and open positions via API2TRADE
3. It runs each active strategy bot as a subprocess
4. Each bot fetches market data (yfinance + API2TRADE), computes indicators, and evaluates its signal conditions
5. If a bot returns `"action": "trade"`, Hermes validates the risk limits
6. If risk passes, `OrderSendSafe` fires directly at the MT5 account via API2TRADE REST
7. Every decision — trade or no trade — is logged to `/var/log/hermes/` and sent to Telegram
The full system uses **zero local MetaTrader installation**, **zero Windows server**, and **zero proprietary SDK**. Just Python and HTTP.
---
## 2. Why REST Instead of a Local MT5 Terminal
The standard approach to MT5 automation is to run an Expert Advisor (EA) in a Windows-based MetaTrader 5 terminal and use either the built-in MQL5 language or the `MetaTrader5` Python package, which requires a local terminal running.
This creates several real problems:
| Problem | Local MT5 Approach | API2TRADE Approach |
|---------|-------------------|-------------------|
| OS requirement | Windows only | Any OS, any cloud |
| Always-on requirement | Terminal must be running 24/5 | API2TRADE handles connectivity |
| Language | MQL5 (proprietary) | Pure Python |
| Deployment | Manual terminal management | `docker compose up` |
| Remote access | RDP into Windows VPS | REST calls from anywhere |
| Broker switching | Reinstall terminal | Change UUID in `.env` |
We originally built GENESIS with a local FastAPI bridge (`127.0.0.1:8000`) that translated Python calls into the MetaTrader5 Python library. This worked, but added a fragile extra layer: the bridge had to be running, the MT5 terminal had to be running, and both had to be on the same Windows machine.
**We removed the bridge entirely** in GENESIS v2.1. Every strategy now calls the API2TRADE REST endpoints directly from Python. The architecture became dramatically simpler:
```
Before (v1): Python → FastAPI Bridge → MetaTrader5 SDK → MT5 Terminal → Broker
After (v2): Python → API2TRADE REST → Broker
```
---
## 3. Core Architecture — How All Parts Connect
![GENESIS Architecture](/Users/sudo/.gemini/antigravity-ide/brain/32917d79-3b86-40d2-b479-1514fff3f799/genesis_architecture_1779961432596.png)
*The complete GENESIS v2.1 architecture: six strategy bots connect directly to MetaTrader 5 via API2TRADE — no local bridge, no Windows server required.*
### File Structure
```
Genesis-Metatrader-Automatic-AI-Trading-System/
├── setup.sh # Interactive setup wizard (run first)
├── install.sh # Ubuntu 22.04 VPS installer
├── docker-compose.yml # Docker deployment
├── core/
│ ├── genesis_autonomous.py # Main engine — runs every 5 min via cron
│ ├── trading_cycle.py # Hermes LLM brain — runs every hour
│ ├── genesis_daily_report.py # 06:00 UTC daily P&L summary
│ ├── genesis_brain_feed.py # :30 min Telegram market summary
│ ├── heartbeat.py # Health check every 10 min
│ └── tg_notify.py # Telegram helper
├── strategies/
│ ├── ares/ # BB+RSI M1 mean reversion
│ │ ├── ares_cycle.py # 643 lines — signal logic + API calls
│ │ └── ares_tool.py # CLI entry point
│ ├── apollo/ # EMA trend following (613 lines)
│ ├── athena/ # BB+RSI multi-TF ranging (622 lines)
│ ├── artemis/ # Ichimoku H1 breakout (498 lines)
│ ├── zeus/ # ICT Smart Money M5 (643 lines)
│ └── hephaestus/ # Grid/Martingale (549 lines)
├── configs/ # YAML config per strategy
└── backtest/ # Python backtester + MT5 EA
```
### The Cron Schedule
```
*/5 * * * * genesis_autonomous.py # Strategy scan — every 5 min
0 * * * * trading_cycle.py # Hermes LLM macro — hourly
30 * * * * genesis_brain_feed.py # Market summary Telegram — :30 min
0 6 * * * genesis_daily_report.py # Daily P&L — 06:00 UTC
0 7 * * 1-5 genesis_market_open.py # Market open alert — weekdays
*/10 * * * * heartbeat.py # Health check — every 10 min
```
---
## 4. The API2TRADE Integration — Every Real Endpoint Used
API2TRADE ([app.api2trade.com](https://app.api2trade.com)) provides a REST API that connects to your live MetaTrader 5 account. You authenticate with Basic HTTP auth on every request, and pass a **session UUID** (the `id` parameter) that identifies which MT5 account you're targeting.
**Authentication:**
```
Username + Password: From your API2TRADE dashboard
Session UUID: Created via /ConnectEx when you link your MT5 account
Base URL: https://mt5.mt4api.dev
```
### Every endpoint GENESIS actually calls:
```python
# ── Account ──────────────────────────────────────────────────────────────────
# Check balance, equity, margin, leverage
GET /AccountSummary?id={uuid}
{"balance": 10000.0, "equity": 10000.0, "currency": "USD",
"leverage": 1000.0, "type": "demo", "method": "Hedging"}
# ── Positions ─────────────────────────────────────────────────────────────────
# All currently open positions
GET /OpenedOrders?id={uuid}
[{"Ticket": 12345, "Symbol": "EURUSDxx", "Type": "Buy",
"Volume": 0.1, "Price": 1.08542, "Profit": 12.30, "Comment": "GENESIS-ARES"}]
# ── Market Data ───────────────────────────────────────────────────────────────
# Live bid/ask quote (used for spread check + entry price)
GET /Quote?id={uuid}&symbol=EURUSDxx
{"Bid": 1.08540, "Ask": 1.08542}
# OHLCV bars (M1, M5, H1, H4, D1)
GET /QuoteHistory?id={uuid}&symbol=EURUSDxx&timeFrame=M5&count=200
# ── Trade Execution ───────────────────────────────────────────────────────────
# Open a position (MT5 "Safe" variant = validates before firing)
GET /OrderSendSafe?id={uuid}&symbol=EURUSDxx&operation=Buy
&volume=0.1&stoploss=1.08392&takeprofit=1.08842&comment=GENESIS-ARES
{"ticket": 12345678}
# Modify SL/TP on open position
GET /OrderModifySafe?id={uuid}&ticket=12345678
&stoploss=1.08450&takeprofit=1.08900
# Close position fully or partially
GET /OrderCloseSafe?id={uuid}&ticket=12345678&lots=0.1
```
### How the bridge() function works inside each strategy:
Every strategy module has a single `bridge()` function that routes logical API calls to the correct endpoint:
```python
# Simplified from ares_cycle.py — the real function is ~70 lines
MT5_API = os.getenv("MT5_API_URL", "https://mt5.mt4api.dev")
MT5_ID = os.getenv("MT5_ACCOUNT_ID", "")
MT5_AUTH = (os.getenv("MT5_API_USER", ""), os.getenv("MT5_API_PASS", ""))
def bridge(path, data=None) -> dict:
"""Single entry point for all MT5 API calls."""
ep_map = {
"/balance": "AccountSummary",
"/positions": "OpenedOrders",
}
if path.startswith("/quote"):
symbol = path.split("symbol=")[-1]
r = requests.get(f"{MT5_API}/Quote",
params={"id": MT5_ID, "symbol": symbol},
auth=MT5_AUTH, timeout=8)
raw = r.json()
return {"bid": raw["Bid"], "ask": raw["Ask"]}
if path == "/market" and data:
r = requests.get(f"{MT5_API}/OrderSendSafe",
params={
"id": MT5_ID,
"symbol": data["symbol"],
"operation": data["type"], # "Buy" or "Sell"
"volume": data["volume"],
"stoploss": data["stop_loss"],
"takeprofit": data["take_profit"],
"comment": data.get("comment", "GENESIS"),
}, auth=MT5_AUTH, timeout=15)
ticket = r.json().get("ticket") or r.json().get("integerResponse")
return {"ticket": ticket}
# AccountSummary, OpenedOrders, etc.
cloud_ep = ep_map.get(path, path.lstrip("/"))
r = requests.get(f"{MT5_API}/{cloud_ep}",
params={"id": MT5_ID}, auth=MT5_AUTH, timeout=10)
return r.json()
```
This design means **adding a new strategy only requires copying one of the existing cycle files and modifying the signal logic** — the API integration layer is already there.
---
## 5. The Six Strategy Gods — Real Implementation Deep Dive
![Strategy Overview](/Users/sudo/.gemini/antigravity-ide/brain/32917d79-3b86-40d2-b479-1514fff3f799/zeus_ict_layers_1779961446917.png)
*Each strategy has a distinct market regime where it performs best. Running all six in parallel means the system is productive in trending, ranging, and volatile conditions.*
Every strategy follows the same interface contract. The `run_analysis(symbol)` function always returns:
```json
{
"action": "trade" | "wait",
"reason": "Human-readable explanation",
"direction": "Buy" | "Sell",
"symbol": "EURUSDxx",
"entry": 1.08542,
"stop_loss": 1.08392,
"take_profit": 1.08842,
"volume": 0.10,
"sl_pips": 15.0,
"rr_ratio": 2.0,
"confidence": "high" | "medium",
"conditions_met": ["RSI oversold", "BB lower touch", ...],
"conditions_failed": [...]
}
```
This uniform interface is what allows `genesis_autonomous.py` to run all six bots and process results identically.
---
### ARES — Bollinger Bands + RSI Mean Reversion (M1)
**File:** `strategies/ares/ares_cycle.py` · 643 lines
ARES is designed for scalping in choppy, ranging markets. The core thesis: when price closes *outside* the Bollinger Band AND RSI is in extreme territory, it tends to snap back to the mean.
**Signal conditions (both required for a trade):**
```python
# BUY signal — from the actual implementation
bull_signal = (
close < bb_lower # Price closed below lower BB (2.0 std dev, 20 period)
and rsi < RSI_OVERSOLD # RSI < 30 (oversold)
and REQUIRE_OUTSIDE # Must be a true band pierce, not just touch
and REQUIRE_RSI # Both conditions must hold simultaneously
)
# Additional strictness gate
# ADX < 25 = ranging market (preferred for mean reversion)
# M15 context MA check = trend not strongly against us
```
**Indicator computation (using the `ta` library):**
```python
def compute_bb_rsi(bars, bb_period=20, bb_dev=2.0, rsi_period=14):
df = pd.DataFrame(bars)
bb_lower = ta.volatility.bollinger_lband(df["close"], window=bb_period, window_dev=bb_dev)
bb_upper = ta.volatility.bollinger_uband(df["close"], window=bb_period, window_dev=bb_dev)
rsi = ta.momentum.rsi(df["close"], window=rsi_period)
adx = ta.trend.adx(df["high"], df["low"], df["close"], window=14)
return {
"bb_lower": float(bb_lower.iloc[-2]), # Last CLOSED bar, not forming bar
"bb_upper": float(bb_upper.iloc[-2]),
"rsi": float(rsi.iloc[-2]),
"adx": float(adx.iloc[-2]),
}
```
**Config parameters (`configs/ares_config.yaml`):**
| Parameter | Value | Reason |
|-----------|-------|--------|
| BB period | 20 | Standard deviation window |
| BB std dev | 2.0 | ~95% price capture |
| RSI period | 14 | Standard Wilder smoothing |
| RSI oversold | 30 | Classic threshold |
| RSI overbought | 70 | Classic threshold |
| Max spread | 1.0 pip | M1 scalp — tight spread critical |
| Session | 07:0021:00 GMT | London + NY overlap |
| SL | 20 pips | Fixed for M1 regime |
| TP | 40 pips | 1:2 minimum R:R |
**Key implementation detail:** We use `iloc[-2]` (second-to-last row) everywhere, not `iloc[-1]`. This is because the last bar is still forming — computing indicators on an incomplete bar causes lookahead bias. This single detail separates a realistic backtest from a profitable-looking one.
---
### APOLLO — EMA 9/21 Trend Following (M5)
**File:** `strategies/apollo/apollo_cycle.py` · 613 lines
APOLLO trades momentum. It only enters when price is moving strongly in one direction, confirmed by two EMAs and volume context.
**Signal logic:**
```python
# EMA crossover + price position
bull_signal = (
ema9 > ema21 # Fast EMA above slow EMA (trend up)
and close > ema9 # Price above both EMAs (momentum confirmed)
and atr > atr_threshold # Sufficient volatility (not dead market)
and rsi > 50 # Momentum bias — not overbought enough to fade
and trend_ma_50 < close # 50-period MA context: overall uptrend
)
```
APOLLO avoids the most common EMA crossover failure mode — entering on a fake cross during consolidation — by requiring **three separate confirmations**: the cross itself, price position relative to both MAs, and ATR-based volatility floor.
---
### ATHENA — Multi-Timeframe BB+RSI Ranging (M5)
**File:** `strategies/athena/athena_cycle.py` · 622 lines
ATHENA is ARES's older sibling. Same BB+RSI core but on M5 (more signal stability, fewer noise trades) with an additional multi-timeframe filter: the H1 chart must not be in a strong trend (ADX check) before ATHENA enters a mean-reversion trade.
The key difference from ARES:
- ARES: fast, M1, tighter spreads, more signals
- ATHENA: slower, M5, wider spread tolerance (1.5 pips), multi-TF confirmation required
---
### ARTEMIS — Ichimoku Kumo Breakout (H1)
**File:** `strategies/artemis/artemis_cycle.py` · 498 lines
ARTEMIS uses the full five-component Ichimoku system: Tenkan-sen (9), Kijun-sen (26), Senkou Span A, Senkou Span B (52), and Chikou Span (26-bar displacement).
**The Ichimoku computation challenge — displacement:**
This is the part that trips up most Ichimoku implementations. Senkou Spans A and B are plotted 26 bars *forward* in the future. To get the cloud at the *current bar*, you read the Span values at index `-(DISP + 2)` in the historical series:
```python
def compute_ichimoku(bars):
DISP = 26 # displacement
tenkan = midpoint(highs, lows, period=9)
kijun = midpoint(highs, lows, period=26)
span_a = (tenkan + kijun) / 2 # Will be plotted DISP bars ahead
span_b = midpoint(highs, lows, period=52) # Will be plotted DISP bars ahead
# Current cloud = values that were calculated DISP bars AGO (now displayed at current bar)
cloud_idx = -(DISP + 2)
sa_current = float(span_a.iloc[cloud_idx]) # ← This is the key
sb_current = float(span_b.iloc[cloud_idx])
kumo_top = max(sa_current, sb_current)
kumo_bottom = min(sa_current, sb_current)
# Chikou Span: current close vs close 26 bars ago
chikou_bullish = close_now > close_disp # Current price above historical
```
**Signal conditions (7 total, minimum 5 must pass for a trade):**
```python
buy_conditions = [
(close > kumo_top, "Price above Kumo"),
(future_cloud == "green", "Future cloud GREEN — SpanA > SpanB ahead"),
(cloud_color == "green", "Current cloud GREEN"),
(chikou_bullish, "Chikou above price 26 bars ago"),
(rsi > 50, "RSI above 50"),
(close > kijun, "Price above Kijun-sen"),
(conf_bars >= CONF_BARS, f"{conf_bars} bars confirmed above Kumo"),
]
# Signal fires only if 5+ conditions pass
if len(passed_conditions) >= 5:
return {"action": "trade", "confidence": "high" if all_7 else "medium"}
```
ARTEMIS is the most selective strategy — it typically generates 28 signals per day on a given symbol and has the highest R:R target (2.5:1 minimum) because H1 setups allow wider SL placement using the Kijun-sen as the natural stop level.
---
### ZEUS — ICT Smart Money Concepts (M5)
**File:** `strategies/zeus/zeus_cycle.py` · 643 lines
ZEUS implements three ICT concepts in **sequential confirmation** — not parallel. All three must activate in order for a trade to fire. This strict sequencing is what makes it high-conviction but low-frequency.
![ICT Detection Pipeline](/Users/sudo/.gemini/antigravity-ide/brain/32917d79-3b86-40d2-b479-1514fff3f799/zeus_ict_layers_1779961446917.png)
**Layer 1 — Liquidity Sweep Detection:**
A liquidity sweep occurs when price briefly exceeds a recent swing high/low (triggering stop orders clustered there) then reverses. The rejection must happen within the same or next candle:
```python
def detect_liquidity_sweep(highs, lows, closes, opens, sym):
# Find swing highs/lows in last 30 bars
sw_highs = detect_swing_highs(highs[-32:-2], lookback=SWING_LB)
sw_lows = detect_swing_lows(lows[-32:-2], lookback=SWING_LB)
# Last closed candle
cur_h = highs[-2]; cur_l = lows[-2]; cur_c = closes[-2]; cur_o = opens[-2]
for level in sw_highs:
wick_above = cur_h > level * (1 + SWEEP_TOL) # Price pierced the level
closed_below = cur_c < level # But closed back below
if wick_above and closed_below and REQ_REJECT:
return {"type": "BearSweep", "level": level, "candle_idx": -2}
```
**Layer 2 — Fair Value Gap (FVG) Detection:**
An FVG is a 3-candle imbalance: the high of candle N is below the low of candle N+2, leaving a gap that price is expected to eventually fill:
```python
def detect_fvg(highs, lows, direction):
# Three-candle imbalance
for i in range(-FVG_AGE, -2):
c1_high = highs[i-1]; c3_low = lows[i+1]
if direction == "bull" and c1_high < c3_low:
gap_size = (c3_low - c1_high) / pip_size
if gap_size >= MIN_GAP_PIPS:
return {"top": c3_low, "bottom": c1_high, "age_bars": abs(i)}
```
**Layer 3 — Order Block Detection:**
The Order Block is the last candle before a strong displacement move — typically a large institutional entry point:
```python
def detect_order_block(bars, direction):
# Find the displacement candle (biggest move in last OB_AGE bars)
# The order block is the candle BEFORE it
# If it's a bullish OB: last bearish candle before bullish surge
# If it's a bearish OB: last bullish candle before bearish dump
body_ratio = abs(close - open) / (high - low)
if body_ratio >= MIN_BODY_RATIO: # Strong displacement candle
ob_candle = bars[displacement_idx - 1]
return {"top": ob_candle["high"], "bottom": ob_candle["low"]}
```
**ICT Confluence Score:**
Each layer contributes points. A minimum score of `MIN_SCORE` (configurable in `zeus_config.yaml`) is required for trade execution. London (07:0010:00 GMT) and New York (13:0016:00 GMT) killzones add a +1 bonus:
```python
score = 0
if liquidity_sweep: score += 2 # Highest weight — sweep is the trigger
if fvg: score += 1 # Confirms displacement
if order_block: score += 2 # Confirms institutional entry zone
if in_killzone(): score += 1 # Time-based bonus
if score >= MIN_SCORE: # Default: 4 out of 6
return {"action": "trade", ...}
```
---
### HEPHAESTUS — Grid / Martingale (Continuous)
**File:** `strategies/hephaestus/hephaestus_cycle.py` · 549 lines
HEPHAESTUS operates differently from the signal-based strategies. It maintains a grid of positions at defined price levels and manages the exposure dynamically. It does not use yfinance for bars — it only needs the current price from the API2TRADE Quote endpoint and the list of open positions.
> ⚠️ **Risk warning:** Grid/Martingale strategies can accumulate significant exposure in trending markets. HEPHAESTUS is included as an implementation example and should be tested thoroughly on demo before using on a live account.
---
## 6. Hermes — The LLM Orchestration Brain
![MT5 API Flow](/Users/sudo/.gemini/antigravity-ide/brain/32917d79-3b86-40d2-b479-1514fff3f799/mt5_api_flow_1779961500293.png)
`trading_cycle.py` runs every hour and asks GPT-4o-mini a structured prompt containing:
- Current account balance and equity
- Open positions and their P&L
- Recent market conditions (pulled from yfinance)
- Which strategies are currently active
- Recent trade journal entries
GPT-4o-mini responds with a structured JSON decision:
```json
{
"market_regime": "ranging",
"preferred_strategies": ["ares", "athena"],
"suppress_strategies": ["apollo"],
"risk_adjustment": 0.8,
"reasoning": "EUR/USD has been oscillating in a 50-pip range since 09:00. Mean reversion strategies favoured. Apollo trend following suppressed until breakout confirmation.",
"macro_notes": "Fed minutes tomorrow 19:00 UTC — reduce position sizes by 20% after 17:00."
}
```
`genesis_autonomous.py` reads these instructions before running each strategy cycle. If Hermes has suppressed a strategy, that bot is skipped. If a risk adjustment is in effect, the calculated lot size is multiplied by the adjustment factor.
**The critical design principle:** Hermes *cannot* override the hard risk limits encoded in each strategy. Even if Hermes tells ARES to trade, ARES still independently checks balance, spread, position count, and R:R. The LLM layer is advisory, not authoritative.
---
## 7. Market Data — Multi-Source Fallback Architecture
GENESIS has a three-tier fallback for price data. This was an engineering necessity: some MT5 brokers (like Deriv Demo) don't stream all quote symbols via the API, and Yahoo Finance blocks Docker container IPs intermittently.
![VPS Deployment](/Users/sudo/.gemini/antigravity-ide/brain/32917d79-3b86-40d2-b479-1514fff3f799/genesis_vps_deployment_1779964695175.png)
**For OHLCV bars (strategy signal computation):**
```python
# yfinance with symbol mapping
YF_MAP = {
"EURUSDxx": "EURUSD=X", "GBPUSDxx": "GBPUSD=X",
"USDJPYxx": "USDJPY=X", "GBPJPYxx": "GBPJPY=X",
"XAUUSDxx": "GC=F",
}
df = yf.download("EURUSD=X", period="5d", interval="1m",
progress=False, auto_adjust=True)
```
**For live quotes (spread check + entry price):**
```python
# Tier 1: API2TRADE /Quote (broker's live feed — best for spread accuracy)
r = requests.get(f"{MT5_API}/Quote",
params={"id": MT5_ID, "symbol": symbol},
auth=MT5_AUTH, timeout=8)
if r.status_code == 200 and r.text.strip():
raw = r.json()
if raw.get("Bid", 0) > 0:
return {"bid": raw["Bid"], "ask": raw["Ask"]}
# Tier 2: open.er-api.com (free, no API key, updated hourly)
r = requests.get(f"https://open.er-api.com/v6/latest/{base_ccy}", timeout=8)
rates = r.json().get("rates", {})
price = rates.get(quote_ccy) # e.g. base=EUR, quote=USD → EURUSD
if price:
spread = 0.00015 # 1.5 pip synthetic spread
return {"bid": price - spread/2, "ask": price + spread/2}
# Tier 3: Frankfurter API (ECB rates, supports XAU/USD)
r = requests.get(f"https://api.frankfurter.app/latest?from=EUR&to=USD", timeout=8)
price = r.json()["rates"]["USD"]
```
**Why this matters:** This fallback chain means GENESIS works correctly even when:
- The MT5 broker quote stream is delayed or unavailable
- You're on a cloud provider that Yahoo Finance rate-limits
- You're testing on a broker that only streams certain symbols
---
## 8. Risk Management — The Hard Layer
Every strategy enforces these checks in order before returning `"action": "trade"`. None can be bypassed by Hermes or any other component.
```
1. Account equity > 0 → "Account equity is zero or unavailable"
2. No duplicate position already open → "Strategy X position already open"
3. Cooldown period (configurable) → "Cooldown: 847s remaining"
4. Is it a trading session? → "Outside session GMT 0721"
5. Spread ≤ max_spread_pips → "Spread 2.3 > max 1.5 pips"
6. No high-impact news ±15 min → "News block: NFP in 8min"
7. Sufficient bar data → "Insufficient M1 bar data"
8. Indicator validity → "Indicator values None"
9. Signal conditions met → "Only 3/7 conditions met"
10. R:R ≥ minimum ratio → "R:R 1.2 < minimum 1.5"
```
**Lot sizing — dynamic, risk-based:**
```python
def calculate_lot(equity, sl_pips, symbol):
# Pip value per lot (approximate)
pip_value = {
"EUR": 10.0, # EURUSD: $10/lot/pip
"GBP": 12.5, # GBPUSD: ~$12.50/lot/pip
"JPY": 9.0, # USDJPY: ~$9/lot/pip
"XAU": 1.0, # XAUUSD: $1/lot/pip (0.1 pip instrument)
}
pv = pip_value.get(symbol[:3], 10.0)
# Risk amount = equity × risk_pct (e.g. 1% of $10,000 = $100)
risk_amount = equity * RISK_PCT # RISK_PCT = 0.01 per strategy
raw_lots = risk_amount / (sl_pips * pv)
return round(max(0.01, min(raw_lots, 3.0)), 2) # Hard cap: 3.0 lots maximum
```
With a $10,000 account, 1% risk, and 20-pip SL on EURUSD:
`$100 ÷ (20 pips × $10/pip) = 0.50 lots`
---
## 9. How to Replicate This — Complete Step-by-Step
### Step 1: Get Your API2TRADE Account
Sign up at **[app.api2trade.com](https://app.api2trade.com)**. Connect your MT5 account by providing your broker login, password, and server name. API2TRADE creates a persistent **session UUID** — this is your `MT5_ACCOUNT_UUID` in the `.env` file.
**Cost:** €12/month per connected MT5 account.
The session UUID looks like: `a1b2c3d4-e5f6-7890-abcd-ef1234567890`
You also get an API username and password for Basic HTTP auth on every request.
### Step 2: Set Up Your Telegram Bot
1. Message [@BotFather](https://t.me/BotFather) on Telegram → `/newbot`
2. Give it a name (e.g. "GENESIS Alerts")
3. Copy the bot token: `1234567890:AABBccDDeeffGGHHiiJJkkLLmmNNoo`
4. Message [@userinfobot](https://t.me/userinfobot) to get your numeric Chat ID
### Step 3: Clone and Configure
```bash
git clone https://github.com/api2trade/Genesis-Metatrader-Automatic-AI-Trading-System.git
cd Genesis-Metatrader-Automatic-AI-Trading-System
# Option A: Interactive wizard (recommended)
bash setup.sh
# → Asks for each credential, verifies them live, writes .env
# Option B: Manual
cp .env.example .env
nano .env # Fill in the 6 required fields
```
The six required `.env` fields:
```bash
MT5_ACCOUNT_UUID=your-api2trade-session-uuid
MT5_ACCOUNT_ID=your-api2trade-session-uuid # same value, alias
MT5_API_USER=your_api2trade_username
MT5_API_PASS=your_api2trade_password
TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_CHAT_ID=your_chat_id
```
### Step 4: Deploy with Docker (Local or VPS)
```bash
# Build and start
docker compose up -d
# Watch startup logs
docker logs -f genesis
# Expected output:
# ✓ MT5_ACCOUNT_UUID = a1b2c3d4••••
# ✓ Verifying API2TRADE connection...
# ✓ Connected | Balance: 10,000.00 USD [demo]
# ✓ 6 cron jobs installed
```
### Step 5: Run Your First Scan
```bash
# Analyze EURUSD across all strategies
docker exec -it genesis genesis-scan EURUSDxx
# Expected output (example):
# === GENESIS FULL SCAN: EURUSDxx ===
# --- ares --- WAIT | Outside session GMT 07-21
# --- apollo --- WAIT | ADX 18.2 < min 20 (trending required)
# --- athena --- WAIT | Spread 1.8 > max 1.5 pips
# --- artemis --- WAIT | Only 3/7 Ichimoku conditions met
# --- zeus --- WAIT | No liquidity sweep detected in last 30 bars
# Or analyze a single strategy in detail
docker exec -it genesis ares analyze EURUSDxx
```
### Step 6: VPS Production Deployment
For 24/7 autonomous trading, a VPS is essential. Minimum specs: **2 vCPU, 2GB RAM, Ubuntu 22.04**. Recommended providers: Hetzner (€4/month), Contabo (€5/month), DigitalOcean (€8/month).
```bash
# On your VPS, after git clone + bash setup.sh:
bash install.sh
# What install.sh does:
# 1. Installs Python 3.11 and pip
# 2. Creates venv at /opt/hermes-agent/.venv-hermes
# 3. Installs all dependencies from requirements.txt
# 4. Creates CLI shortcuts: ares, apollo, athena, artemis, zeus, hephaestus, genesis-scan
# 5. Writes /etc/cron.d/genesis with the 6 cron jobs
# 6. Creates /var/log/hermes/ log directories
# 7. Sends a test Telegram message confirming the installation
```
### Step 7: Customise a Strategy
Each strategy is a self-contained Python module. To modify ARES's signal thresholds without touching code:
```yaml
# configs/ares_config.yaml
bollinger:
period: 20 # ← Change BB window
std_dev: 2.5 # ← Wider band = fewer but higher quality signals
rsi:
period: 14
oversold: 25 # ← Stricter oversold threshold (was 30)
overbought: 75 # ← Stricter overbought threshold
risk:
risk_pct: 0.01 # ← 1% per trade
max_spread_pips: 1.0
min_rr_ratio: 2.0 # ← Minimum 1:2 reward-to-risk
```
To write a completely new strategy:
1. Copy `strategies/ares/` to `strategies/mybot/`
2. Rename `ares_cycle.py` to `mybot_cycle.py` and `ares_tool.py` to `mybot_tool.py`
3. Replace the `run_analysis()` logic with your signal conditions
4. Keep the `bridge()` function and the return format unchanged
5. Add your strategy to `genesis_autonomous.py`'s bot list
---
## 10. Infrastructure and Real Costs
### Monthly Running Costs
| Service | Cost | What for |
|---------|------|----------|
| API2TRADE | **€12/month** | MT5 REST API — 1 account |
| VPS (Hetzner CX21) | **€4.35/month** | 2 vCPU, 4GB RAM, Ubuntu 22.04 |
| OpenAI (GPT-4o-mini) | **~€515/month** | Hermes LLM brain — hourly cycles |
| Telegram Bot | **Free** | All trade alerts and reports |
| open.er-api.com | **Free** | Quote fallback for Forex pairs |
| yfinance | **Free** | OHLCV bars for all strategies |
| **Total** | **~€2131/month** | Full autonomous system |
### Development Investment (AI Tokens)
GENESIS was built over approximately 3 weeks using AI-assisted development (Claude/Gemini models). The estimated token usage across all development conversations:
| Phase | Description | Approx. Tokens |
|-------|-------------|----------------|
| Architecture design | System design, data flow decisions | ~200K |
| Strategy implementation | All 6 strategy bots coded + debugged | ~800K |
| Bridge removal refactor | Moving from local bridge to direct API | ~200K |
| Docker + deployment | Dockerfile, docker-compose, entrypoints | ~150K |
| Documentation + README | Case study, README, .env.example | ~200K |
| Debugging sessions | Auth issues, import paths, yfinance | ~250K |
| **Total** | | **~1.8M tokens** |
At current Claude/Gemini pricing (~$315 per million tokens depending on model), total AI-assisted development cost: **approximately $527**.
Compare this to hiring a professional quant developer at €100200/hour to build the equivalent system. GENESIS represents roughly 200+ hours of equivalent development work.
---
## 11. Key Engineering Challenges and How We Solved Them
### Challenge 1: The Local Bridge Was a Single Point of Failure
**Problem:** The original v1 used a FastAPI bridge at `localhost:8000` that translated Python calls into the MetaTrader5 Python SDK. This meant three things had to be running simultaneously: the FastAPI bridge, the MT5 terminal, and the Python strategies. Any one crashing silently would stop all trading.
**Solution:** Remove the bridge entirely. Every `*_cycle.py` file now has a `bridge()` function that calls API2TRADE REST directly. The strategies became self-contained — each one can run independently without any other service.
```python
# Before (v1) — required local bridge running
def bridge(path, data=None):
r = requests.post(f"http://127.0.0.1:8000{path}", json=data)
return r.json()
# After (v2) — direct REST call
def bridge(path, data=None):
r = requests.get(f"{MT5_API}/AccountSummary",
params={"id": MT5_ID}, auth=MT5_AUTH, timeout=10)
return r.json()
```
### Challenge 2: Ichimoku Displacement — The Lookahead Trap
**Problem:** Every Ichimoku tutorial shows the cloud "shifted forward" visually, but when computing it in code, most implementations accidentally read the **future** cloud values at `iloc[-1]` instead of the **current** cloud projected at `iloc[-(DISP+2)]`. This creates a massive lookahead bias — the strategy "sees" cloud values that won't exist yet in real-time.
**Solution:** Explicit displacement indexing. The current Kumo boundaries are the SpanA/B values calculated `DISP` bars ago:
```python
cloud_idx = -(DISP + 2) # DISP = 26 bars displacement
sa_current = float(span_a.iloc[cloud_idx]) # ← Correct
# NOT: span_a.iloc[-1] ← This is lookahead bias
```
### Challenge 3: Yahoo Finance Rate Limiting Inside Docker
**Problem:** When testing GENESIS in Docker on a Mac, `yf.download("EURUSD=X", ...)` consistently returned empty DataFrames. Yahoo Finance blocks or rate-limits requests from Docker container IP ranges.
**Solution:** Three-tier quote fallback using free public APIs that don't rate-limit Docker IPs:
1. API2TRADE `/Quote` (broker's live feed)
2. `open.er-api.com` (free ECB/central bank rates, no API key)
3. `Frankfurter.app` (ECB reference rates, supports XAU/USD)
In production on a VPS, yfinance works normally. The fallback ensures correctness in all environments.
### Challenge 4: `iloc[-2]` vs `iloc[-1]` — Forming Bar Lookahead
**Problem:** When downloading 1-minute bars, the last row (`iloc[-1]`) is the bar currently forming — it has incomplete data. Computing indicators on it means your signal conditions are evaluated against a partial candle that will change before the bar closes.
**Solution:** Every indicator computation uses `iloc[-2]` — the last fully closed bar. This is implemented consistently across all six strategies:
```python
# Every strategy uses this pattern
bb_lower = float(ta.volatility.bollinger_lband(df["close"], window=20).iloc[-2])
rsi = float(ta.momentum.rsi(df["close"], window=14).iloc[-2])
# Never iloc[-1] for signal computation
```
### Challenge 5: Symbol Format Differences Between Brokers
**Problem:** Different MT5 brokers use different symbol names. Exness uses `EURUSDxx`, Deriv uses `EURUSD`, IC Markets uses `EURUSD`, Pepperstone uses `EURUSD.`. A system hardcoded to `EURUSDxx` breaks silently on other brokers.
**Solution:** Configurable symbol suffixes in YAML configs, plus a `YF_MAP` dictionary that maps MT5 symbols to their yfinance equivalents regardless of broker suffix:
```yaml
# configs/ares_config.yaml
mt5:
symbol_suffix: "xx" # Change to "" for brokers without suffix
```
```python
YF_MAP = {
"EURUSDxx": "EURUSD=X",
"EURUSD": "EURUSD=X", # Works with any suffix variant
"EURUSD.": "EURUSD=X",
}
```
---
## 12. Live Test Results — Deriv Demo Account
We validated GENESIS v2.1 on a **Deriv Demo account** ($10,000 USD, 1:1000 leverage) via API2TRADE before the open-source release. Here is what actually happened:
**API2TRADE Session Connection:**
```
ConnectEx → 200 OK → Session UUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
AccountSummary:
balance: $10,000.00 USD
equity: $10,000.00 USD
leverage: 1000:1
type: demo
method: Hedging
```
**Full Five-Strategy Scan on EURUSDxx:**
```
=== GENESIS FULL SCAN — Deriv Demo $10k ===
ARES → WAIT | Spread 1.50 pips > max 1.0 pips
APOLLO → WAIT | Spread 1.50 pips > max 1.5 pips
ATHENA → WAIT | Spread 1.50 pips > max 1.5 pips
ARTEMIS → WAIT | Insufficient H1 data (yfinance blocked in Docker)
ZEUS → WAIT | Insufficient M5 data (yfinance blocked in Docker)
```
**Reading these results correctly:**
- ARES, APOLLO, ATHENA all **reached the spread check gate** — meaning they successfully: connected to API2TRADE, fetched account balance ($10k confirmed), checked for open positions, validated the trading session, and obtained a live price quote (via open.er-api.com fallback at 1.50 pip synthetic spread). The spread rejection is correct behaviour — ARES's max is 1.0 pip for M1 scalping.
- ARTEMIS and ZEUS returned "Insufficient data" because yfinance is blocked from Docker on this test machine. On a VPS, yfinance returns full H1 and M5 data normally, and both strategies complete their full analysis.
**In other words:** The entire API2TRADE integration, authentication, account data, quote retrieval, and risk layer worked correctly. The "WAIT" decisions are not failures — they are the system correctly declining to trade based on real conditions.
---
## 13. What to Do Next
If you are reading this as a developer looking to build your own system:
**Minimum viable starting point:**
1. Sign up at [app.api2trade.com](https://app.api2trade.com) — get your session UUID
2. Clone this repo and run `bash setup.sh`
3. Run `docker exec -it genesis ares analyze EURUSDxx` — confirm it connects to your account
4. Spend 1 week watching the strategy outputs on demo before enabling execution
**If you want to customise the strategies:**
- ARES's `ares_config.yaml` is the cleanest to start with — change `bb_period`, `std_dev`, and `rsi_oversold` thresholds and observe the effect on signal frequency
- ZEUS is the most institutionally-aligned — if you're familiar with ICT concepts, this is the most interesting codebase to extend
**If you want to scale across multiple accounts:**
- API2TRADE supports multiple connected MT5 accounts — each gets its own session UUID
- You can run multiple `docker-compose` stacks with different `.env` files pointing to different accounts
- Each additional account costs €12/month
**The open-source repository** includes everything: all six strategy implementations, all YAML configs, the autonomous engine, Hermes LLM brain, Docker deployment, VPS installer, and the interactive setup wizard. Fork it, adapt it, and build on top of it.
> **Get started: [app.api2trade.com](https://app.api2trade.com)**
---
*GENESIS is open-source under GPL-3.0. Forks must also remain open source.*
*Trading involves risk. Always test on a demo account before using real funds.*
*Published by API2TRADE · https://app.api2trade.com*
+78
View File
@@ -0,0 +1,78 @@
# ─────────────────────────────────────────────────────────────────────────────
# GENESIS Trading System — Docker Image
# Base: Ubuntu 22.04 (matches production VPS)
# ─────────────────────────────────────────────────────────────────────────────
FROM ubuntu:22.04
# Avoid interactive prompts during apt
ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# ── System packages ───────────────────────────────────────────────────────────
RUN apt-get update && apt-get install -y \
python3.11 \
python3.11-venv \
python3-pip \
cron \
curl \
tzdata \
&& rm -rf /var/lib/apt/lists/*
# Set timezone to UTC (matches trading sessions)
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# ── Install directory (mirrors production /opt/hermes-agent) ──────────────────
RUN mkdir -p /opt/hermes-agent
WORKDIR /opt/hermes-agent
# ── Python virtual environment ────────────────────────────────────────────────
RUN python3.11 -m venv /opt/hermes-agent/.venv-hermes
ENV PATH="/opt/hermes-agent/.venv-hermes/bin:$PATH"
# ── Install Python dependencies ───────────────────────────────────────────────
COPY requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt
# ── Copy all source files ─────────────────────────────────────────────────────
COPY core/ ./core/
COPY strategies/ ./strategies/
COPY configs/ ./configs/
COPY backtest/ ./backtest/
COPY services/ ./services/
COPY .env.example .
# ── Copy configs into each strategy subfolder (where *_cycle.py expects them) ─
RUN cp configs/ares_config.yaml strategies/ares/ && \
cp configs/apollo_config.yaml strategies/apollo/ && \
cp configs/athena_config.yaml strategies/athena/ && \
cp configs/artemis_config.yaml strategies/artemis/ && \
cp configs/zeus_config.yaml strategies/zeus/ && \
cp configs/hephaestus_config.yaml strategies/hephaestus/
# ── Create log directories ────────────────────────────────────────────────────
RUN mkdir -p \
/var/log/hermes \
/var/log/ares \
/var/log/apollo \
/var/log/athena \
/var/log/artemis \
/var/log/zeus \
/var/log/hephaestus
# ── CLI shortcuts ──────────────────────────────────────────────────────────────
RUN for bot in ares apollo athena artemis zeus hephaestus; do \
printf '#!/bin/bash\n/opt/hermes-agent/.venv-hermes/bin/python3 /opt/hermes-agent/strategies/%s/%s_tool.py "$@"\n' \
"$bot" "$bot" > /usr/local/bin/$bot && \
chmod +x /usr/local/bin/$bot; \
done
RUN printf '#!/bin/bash\nSYMBOL=${1:-EURUSDxx}\necho ""\necho "=== GENESIS FULL SCAN: $SYMBOL ==="\necho ""\nfor bot in ares apollo athena artemis zeus; do\n echo "--- $bot ---"\n $bot analyze $SYMBOL 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'\'' Action: {d.get(chr(34)+chr(97)+chr(99)+chr(116)+chr(105)+chr(111)+chr(110)+chr(34),chr(63))}'\'')"\ndone\necho ""\n' \
> /usr/local/bin/genesis-scan && chmod +x /usr/local/bin/genesis-scan
# ── Entrypoint ────────────────────────────────────────────────────────────────
COPY docker-entrypoint.sh /entrypoint.sh
RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
File diff suppressed because it is too large Load Diff
+379
View File
@@ -0,0 +1,379 @@
# GENESIS — Autonomous MT5 Trading System
> **Powered by [API2TRADE](https://app.api2trade.com)** — the REST API for MetaTrader 5
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Python 3.11](https://img.shields.io/badge/Python-3.11-brightgreen)](https://python.org)
[![MT5 via API2TRADE](https://img.shields.io/badge/MT5-API2TRADE-orange)](https://app.api2trade.com)
GENESIS is a fully autonomous, multi-strategy Forex trading system that connects to any **MetaTrader 5** account via the [API2TRADE](https://app.api2trade.com) REST API. It runs six independent strategy bots in parallel, managed by **Hermes** — an LLM-powered orchestration brain.
---
## 🏗 Architecture
```
┌─────────────────────────────┐
│ HERMES (Brain) │
│ GPT-4o-mini · Hourly LLM │
│ Macro analysis + routing │
└──────────────┬──────────────┘
┌────────────────────────┼────────────────────────┐
│ GENESIS Autonomous Engine │
│ (genesis_autonomous.py · every 5min) │
└──┬──────┬──────┬──────┬──────┬──────────────────┘
│ │ │ │ │
ARES APOLLO ATHENA ARTEMIS ZEUS HEPHAESTUS
M1 M5 M5 H1 M5 Grid
BB+RSI EMA BB+RSI Ichimoku ICT Martingale
│ │ │ │ │ │
└──────┴──────┴──────┴──────┴────────┘
┌─────────▼─────────┐
│ API2TRADE REST │
│ app.api2trade.com│
└─────────┬─────────┘
┌─────────▼─────────┐
│ MetaTrader 5 │
│ (any broker) │
└───────────────────┘
```
---
## 🤖 Strategy Bots
| Bot | Timeframe | Strategy | Symbols | Expected Signals |
|-----|-----------|----------|---------|-----------------|
| **ARES** | M1 | Bollinger Bands + RSI mean reversion | EURUSD, GBPUSD | 1030/day |
| **APOLLO** | M5 | EMA 9/21 crossover trend following | EURUSD, GBPUSD | 515/day |
| **ATHENA** | M5 | Multi-TF BB + RSI ranging | EURUSD | 515/day |
| **ARTEMIS** | H1 | Ichimoku Kumo breakout | EURUSD, GBPUSD, GBPJPY | 28/day |
| **ZEUS** | M5 | ICT Smart Money — Liquidity + FVG + OB | EURUSD, GBPUSD, XAUUSD | 310/day |
| **HEPHAESTUS** | — | Grid / Martingale cycling | Any | Continuous |
---
## 📋 Prerequisites
### 1. API2TRADE Account (Required)
GENESIS communicates with MT5 exclusively through the [API2TRADE](https://app.api2trade.com) REST API — no local MT5 installation, no Windows VPS required.
1. Sign up at **[app.api2trade.com](https://app.api2trade.com)**
2. Connect your MetaTrader 5 account
3. Copy your **Account UUID** and **API Key** from the dashboard
> **Cost:** €12/month per connected MT5 account · No per-call fees · Unlimited requests
### 2. Telegram Bot (Required for alerts)
1. Message [@BotFather](https://t.me/BotFather) → `/newbot`
2. Copy the bot token
3. Get your Chat ID from [@userinfobot](https://t.me/userinfobot)
### 3. OpenAI API Key (Optional — for Hermes brain)
1. Get a key at [platform.openai.com/api-keys](https://platform.openai.com/api-keys)
2. Cost: ~$0.100.50/day using `gpt-4o-mini`
3. Without it: GENESIS runs strategies autonomously without LLM macro analysis
---
## 🚀 Quick Start
### Option A — Docker (Recommended for local testing)
```bash
# 1. Clone the repo
git clone https://github.com/api2trade/Genesis-Metatrader-Automatic-AI-Trading-System.git
cd Genesis-Metatrader-Automatic-AI-Trading-System
# 2. Run the setup wizard (generates your .env)
bash setup.sh
# 3. Start the container
docker compose up -d
# 4. Watch it run
docker logs -f genesis
```
### Option B — VPS Production (Ubuntu 22.04)
```bash
# 1. Clone the repo on your VPS
git clone https://github.com/api2trade/Genesis-Metatrader-Automatic-AI-Trading-System.git
cd Genesis-Metatrader-Automatic-AI-Trading-System
# 2. Run setup wizard
bash setup.sh
# 3. Install cron jobs, CLI shortcuts and log folders
bash install.sh
# 4. Verify cron schedule is active
crontab -l
# 5. Watch live logs
tail -f /var/log/hermes/autonomous.log
```
---
## 🔧 Manual Setup
If you prefer to configure manually instead of using `setup.sh`:
```bash
cp .env.example .env
nano .env # Fill in your credentials
```
Required fields:
| Variable | Where to get it |
|----------|----------------|
| `MT5_ACCOUNT_UUID` | [app.api2trade.com](https://app.api2trade.com) → Dashboard |
| `MT5_API_KEY` | [app.api2trade.com](https://app.api2trade.com) → API Keys |
| `MT5_API_USER` | Your API2TRADE username |
| `MT5_API_PASS` | Your API2TRADE password |
| `TELEGRAM_BOT_TOKEN` | [@BotFather](https://t.me/BotFather) |
| `TELEGRAM_CHAT_ID` | [@userinfobot](https://t.me/userinfobot) |
---
## 💻 Usage
### Strategy Analysis
```bash
# Single strategy analysis (no trade placed)
docker exec -it genesis ares analyze EURUSDxx
docker exec -it genesis apollo analyze GBPUSDxx
docker exec -it genesis athena analyze EURUSDxx
docker exec -it genesis artemis analyze GBPUSDxx
docker exec -it genesis zeus analyze XAUUSDxx
# Scan ALL strategies on one symbol at once
docker exec -it genesis genesis-scan EURUSDxx
docker exec -it genesis genesis-scan GBPUSDxx
```
### Live Logs
```bash
# All decisions
docker exec -it genesis tail -f /var/log/hermes/autonomous.log
# Trade journal (JSONL)
docker exec -it genesis tail -f /var/log/hermes/trade_journal.jsonl
# LLM macro cycles
docker exec -it genesis tail -f /var/log/hermes/trading_cycle.log
```
### Strategy Output Format
Every strategy returns a JSON object:
```json
{
"action": "trade",
"strategy": "ares-bb-rsi-m1",
"symbol": "EURUSDxx",
"direction": "Buy",
"entry": 1.08542,
"stop_loss": 1.08392,
"take_profit": 1.08842,
"volume": 0.1,
"rr_ratio": 2.0,
"sl_pips": 15.0,
"confidence": "high",
"reason": "RSI oversold + BB lower touch + session active"
}
```
---
## 📁 Project Structure
```
Genesis-Metatrader-Automatic-AI-Trading-System/
├── setup.sh # ← Interactive setup wizard (start here)
├── install.sh # ← VPS production installer
├── docker-compose.yml # ← Docker deployment
├── Dockerfile
├── .env.example # ← Credential template
├── core/
│ ├── genesis_autonomous.py # Main engine — runs every 5min via cron
│ ├── trading_cycle.py # Hermes LLM macro cycle — hourly
│ ├── genesis_daily_report.py # Daily P&L Telegram report
│ ├── genesis_brain_feed.py # Hourly Telegram market summary
│ ├── heartbeat.py # System health check
│ └── tg_notify.py # Telegram helper
├── strategies/
│ ├── ares/ # BB+RSI M1 mean reversion
│ │ ├── ares_cycle.py # Strategy logic + API2TRADE bridge
│ │ └── ares_tool.py # CLI: ares analyze/execute EURUSDxx
│ ├── apollo/ # EMA trend following
│ ├── athena/ # BB+RSI multi-TF ranging
│ ├── artemis/ # Ichimoku H1 breakout
│ ├── zeus/ # ICT Smart Money M5
│ └── hephaestus/ # Grid/Martingale
├── configs/
│ ├── ares_config.yaml # ARES parameters (BB period, RSI thresholds, etc.)
│ ├── apollo_config.yaml
│ ├── athena_config.yaml
│ ├── artemis_config.yaml
│ ├── zeus_config.yaml
│ └── hephaestus_config.yaml
└── backtest/
├── backtest.py # Python backtester using yfinance
└── BB_RSI_MeanReversion.mq5 # MT5 Expert Advisor (manual backtest)
```
---
## ⚡ API2TRADE — How It Works
GENESIS uses API2TRADE as the bridge between Python and MetaTrader 5. Every strategy calls these endpoints directly:
```python
# Get account balance
GET /AccountSummary?id={session_uuid}
{"balance": 10000.0, "equity": 10000.0, "currency": "USD"}
# Get live quote
GET /Quote?id={session_uuid}&symbol=EURUSDxx
{"Bid": 1.08540, "Ask": 1.08542}
# Place a trade
GET /OrderSendSafe?id={session_uuid}&symbol=EURUSDxx&operation=Buy&volume=0.1&stoploss=1.083&takeprofit=1.090&comment=GENESIS-ARES
{"ticket": 12345678}
# Close a position
GET /OrderCloseSafe?id={session_uuid}&ticket=12345678&lots=0.1
```
No WebSocket setup, no local MT5 terminal, no Windows server. Just REST calls from any Python environment.
> 💡 **Sign up at [app.api2trade.com](https://app.api2trade.com) to get your session UUID and API key.**
---
## 🛡 Risk Management
Hard limits enforced before **every** trade — cannot be bypassed:
| Limit | Default | Config |
|-------|---------|--------|
| Max risk per trade | **2% of balance** | `MAX_RISK_PCT` in `.env` |
| Max lot size | **3.0 lots** | `MAX_LOTS` in `.env` |
| Max open positions | **4** | `MAX_POSITIONS` in `.env` |
| Stop loss | **Required** (5150 pips) | Per strategy config |
| Spread filter | **1.03.0 pips** | Per strategy YAML |
| News filter | **±15 min** around high-impact | Per strategy YAML |
---
## ⚙️ Configuration
Each strategy has a dedicated YAML config in `configs/`. Example for ARES:
```yaml
# configs/ares_config.yaml
strategy:
name: ARES
magic_number: 1001
bollinger:
period: 20
std_dev: 2.0
rsi:
period: 14
oversold: 30
overbought: 70
risk:
risk_pct: 0.01 # 1% per trade
max_spread_pips: 1.0
min_rr_ratio: 1.5
sessions:
allowed:
- {start: 7, end: 20} # GMT hours
```
---
## 🖥 VPS Deployment (Recommended)
For 24/7 autonomous trading, deploy on a Ubuntu 22.04 VPS:
```bash
# Minimum spec: 2 vCPU, 2GB RAM, 20GB SSD
# Cost: ~€48/month (Hetzner, Contabo, DigitalOcean)
bash setup.sh # Configure credentials
bash install.sh # Install Python, venv, cron jobs, CLI shortcuts
```
The installer sets up:
- Python 3.11 virtual environment at `/opt/hermes-agent/.venv-hermes`
- Cron job: `genesis_autonomous.py` every 5 minutes
- Cron job: `trading_cycle.py` (Hermes LLM) every hour
- Log rotation at `/var/log/hermes/`
- CLI shortcuts: `ares`, `apollo`, `athena`, `artemis`, `zeus`, `hephaestus`, `genesis-scan`
---
## 📊 Backtesting
```bash
# Backtest ARES on EURUSD (last 12 months)
python3 backtest/backtest.py --strategy ares --symbol EURUSD --days 365
# Or open the MT5 EA in MetaEditor for native backtesting
# backtest/BB_RSI_MeanReversion.mq5
```
---
## 🤝 Contributing
GENESIS is open source under **GPL v3** — forks must also be open source.
1. Fork the repo
2. Create your feature branch (`git checkout -b feature/my-strategy`)
3. Commit your changes
4. Open a Pull Request
---
## ⚠️ Disclaimer
This software is for **educational and research purposes**. Forex trading involves substantial risk of loss. Past performance does not guarantee future results. Always test on a **demo account** before trading with real money. The authors accept no responsibility for financial losses.
---
## 📄 License
GPL-3.0 · See [LICENSE](LICENSE)
---
<div align="center">
**Built with API2TRADE · [app.api2trade.com](https://app.api2trade.com)**
*Connect any MT5 account to Python in minutes · €12/month per account*
</div>
+597
View File
@@ -0,0 +1,597 @@
//+------------------------------------------------------------------+
//| BB_RSI_MeanReversion.mq5 |
//| Version: 1.0 |
//| Description: Mean reversion EA using Bollinger Bands + RSI |
//| on M1 with optional M15 higher-timeframe context. |
//| |
//| RISK WARNING: This EA is for educational purposes only. |
//| Live trading requires proper risk assessment, forward testing, |
//| and understanding of all risks involved in Forex trading. |
//| Past performance does not guarantee future results. |
//+------------------------------------------------------------------+
#property copyright "GENESIS Strategy B — Ares"
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
//+------------------------------------------------------------------+
//| INPUT GROUPS |
//+------------------------------------------------------------------+
// --- 1. Trade Filters ---
input string Inp_TradeComment = "BB_RSI_M1"; // EA comment
input bool Inp_AllowLong = true; // Allow long trades
input bool Inp_AllowShort = true; // Allow short trades
input int Inp_MagicNumber = 20250514; // EA magic number
// --- 2. Bollinger Bands ---
input int Inp_BB_Period = 20; // BB period
input double Inp_BB_Deviation = 2.0; // BB deviation
input int Inp_BB_Shift = 0; // BB shift
input ENUM_MA_METHOD Inp_BB_MA_Method = MODE_SMA; // BB MA method
input ENUM_APPLIED_PRICE Inp_BB_Price = PRICE_CLOSE; // BB applied price
// --- 3. RSI ---
input int Inp_RSI_Period = 14; // RSI period
input double Inp_RSI_Oversold = 30.0; // RSI oversold level
input double Inp_RSI_Overbought = 70.0; // RSI overbought level
input ENUM_APPLIED_PRICE Inp_RSI_Price = PRICE_CLOSE; // RSI applied price
// --- 4. Higher Timeframe Context (M15) ---
input bool Inp_UseM15Context = true; // Use M15 context
input ENUM_TIMEFRAMES Inp_ContextTF = PERIOD_M15; // Context timeframe
input int Inp_ContextMAPeriod = 50; // Context MA period
input double Inp_ContextMATol = 0.0002; // Distance tolerance from MA
// --- 5. Entry Logic ---
input bool Inp_RequireOutsideBand = true; // Price must close outside BB
input bool Inp_RequireRSIFilter = true; // Require RSI filter
input int Inp_CandlesSinceSignal = 1; // Candle index (1=last closed)
// --- 6. Risk & Money Management ---
input double Inp_RiskPercent = 1.0; // % account risked per trade
input bool Inp_UseFixedLot = false; // Use fixed lot
input double Inp_FixedLot = 0.01; // Fixed lot size
input int Inp_StopLossPips = 20; // SL in pips
input int Inp_TakeProfitPips = 40; // TP in pips
input bool Inp_UseTrailingStop = false; // Enable trailing stop
input int Inp_TrailingStartPips = 15; // Profit pips to start trailing
input int Inp_TrailingStepPips = 5; // Trailing step in pips
// --- 7. Time & Session Filters ---
input bool Inp_UseTimeFilter = true; // Restrict trading hours
input int Inp_StartHour = 5; // Start hour (GMT)
input int Inp_StartMinute = 0; // Start minute
input int Inp_EndHour = 17; // End hour (GMT)
input int Inp_EndMinute = 0; // End minute
input bool Inp_UseNewsFilter = true; // Avoid news events
input string Inp_NewsFile = "news.txt"; // News timestamps file
// --- 8. Spread & Slippage ---
input double Inp_MaxSpreadPips = 1.0; // Max allowed spread (pips)
input int Inp_Slippage = 10; // Slippage tolerance (points)
input int Inp_MaxRetries = 3; // Max order send retries
// --- 9. Drawdown Protection ---
input bool Inp_UseDailyLossLimit = true; // Stop after daily loss
input double Inp_DailyLossPercent = 6.0; // Max daily loss %
input bool Inp_UseGlobalDDLimit = true; // Global drawdown halt
input double Inp_GlobalDDPercent = 25.0; // Max total DD %
input bool Inp_CloseAllOnDD = true; // Close all on DD breach
// --- 10. Execution ---
input bool Inp_UseOnePositionPerDir = true; // One position per direction
input int Inp_MinSecondsBetweenTrades = 30; // Cooldown seconds
//+------------------------------------------------------------------+
//| GLOBAL VARIABLES |
//+------------------------------------------------------------------+
CTrade g_Trade;
CPositionInfo g_Position;
int g_BB_Handle = INVALID_HANDLE;
int g_RSI_Handle = INVALID_HANDLE;
int g_MA_Handle = INVALID_HANDLE;
double g_PipSize = 0.0;
double g_PeakEquity = 0.0;
double g_DayStartBal = 0.0;
datetime g_LastBarTime = 0;
datetime g_LastTradeCloseTime = 0;
bool g_TradingDisabled = false;
datetime g_CurrentDayStart = 0;
datetime g_NewsTimes[];
int g_NewsCount = 0;
int g_NewsMinutes = 15; // minutes before/after to block
//+------------------------------------------------------------------+
//| OnInit |
//+------------------------------------------------------------------+
int OnInit()
{
// Determine pip size (4-digit vs 5-digit broker)
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
g_PipSize = (digits == 3 || digits == 5) ? _Point * 10.0 : _Point;
// Create indicator handles
g_BB_Handle = iBands(_Symbol, PERIOD_M1, Inp_BB_Period, Inp_BB_Shift,
Inp_BB_Deviation, Inp_BB_Price);
g_RSI_Handle = iRSI(_Symbol, PERIOD_M1, Inp_RSI_Period, Inp_RSI_Price);
g_MA_Handle = iMA(_Symbol, Inp_ContextTF, Inp_ContextMAPeriod, 0,
MODE_SMA, PRICE_CLOSE);
if(g_BB_Handle == INVALID_HANDLE ||
g_RSI_Handle == INVALID_HANDLE ||
g_MA_Handle == INVALID_HANDLE)
{
Print("ERROR: Failed to create indicator handles. EA stopping.");
return INIT_FAILED;
}
// Configure trade object
g_Trade.SetExpertMagicNumber(Inp_MagicNumber);
g_Trade.SetDeviationInPoints(Inp_Slippage);
g_Trade.SetTypeFilling(ORDER_FILLING_FOK);
// Initialise equity tracking
g_PeakEquity = AccountInfoDouble(ACCOUNT_EQUITY);
g_DayStartBal = AccountInfoDouble(ACCOUNT_BALANCE);
g_CurrentDayStart = GetDayStart(TimeCurrent());
// Load news filter file
if(Inp_UseNewsFilter) LoadNewsFile();
Print("BB_RSI_MeanReversion EA initialised. PipSize=", g_PipSize,
" | Magic=", Inp_MagicNumber);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| OnDeinit |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_BB_Handle != INVALID_HANDLE) IndicatorRelease(g_BB_Handle);
if(g_RSI_Handle != INVALID_HANDLE) IndicatorRelease(g_RSI_Handle);
if(g_MA_Handle != INVALID_HANDLE) IndicatorRelease(g_MA_Handle);
}
//+------------------------------------------------------------------+
//| OnTick |
//+------------------------------------------------------------------+
void OnTick()
{
// 0. If globally disabled, just manage trailing on existing positions
if(g_TradingDisabled)
{
if(Inp_UseTrailingStop) ManageTrailingStop();
return;
}
// 1. Only act on new bar
if(!IsNewBar()) return;
// 2. Update peak equity
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity > g_PeakEquity) g_PeakEquity = equity;
// 3. Reset day tracking if new day
datetime today = GetDayStart(TimeCurrent());
if(today != g_CurrentDayStart)
{
g_CurrentDayStart = today;
g_DayStartBal = AccountInfoDouble(ACCOUNT_BALANCE);
Print("New trading day. Starting balance: ", g_DayStartBal);
}
// 4. Global drawdown check
if(Inp_UseGlobalDDLimit && g_PeakEquity > 0)
{
double ddPct = (g_PeakEquity - equity) / g_PeakEquity * 100.0;
if(ddPct >= Inp_GlobalDDPercent)
{
Print("GLOBAL DRAWDOWN LIMIT HIT: ", DoubleToString(ddPct, 2),
"% >= ", Inp_GlobalDDPercent, "%. Halting EA.");
if(Inp_CloseAllOnDD) CloseAllPositions();
g_TradingDisabled = true;
return;
}
}
// 5. Daily loss check
if(Inp_UseDailyLossLimit && g_DayStartBal > 0)
{
double dayLossPct = (g_DayStartBal - AccountInfoDouble(ACCOUNT_BALANCE))
/ g_DayStartBal * 100.0;
if(dayLossPct >= Inp_DailyLossPercent)
{
Print("DAILY LOSS LIMIT HIT: ", DoubleToString(dayLossPct, 2),
"% >= ", Inp_DailyLossPercent, "%. Skipping until tomorrow.");
return;
}
}
// 6. Time filter
if(Inp_UseTimeFilter && !IsTradeTime()) return;
// 7. Spread filter
double spreadPips = GetCurrentSpreadPips();
if(spreadPips > Inp_MaxSpreadPips)
{
Print("Spread too high: ", DoubleToString(spreadPips, 2),
" pips > max ", Inp_MaxSpreadPips);
return;
}
// 8. News filter
if(Inp_UseNewsFilter && IsNewsTime()) return;
// 9. Cooldown check
if((int)(TimeCurrent() - g_LastTradeCloseTime) < Inp_MinSecondsBetweenTrades)
return;
// 10. Get indicator values
double bbUpper[], bbLower[], bbMiddle[];
double rsiVal[];
ArraySetAsSeries(bbUpper, true);
ArraySetAsSeries(bbLower, true);
ArraySetAsSeries(bbMiddle, true);
ArraySetAsSeries(rsiVal, true);
int idx = Inp_CandlesSinceSignal; // 1 = last closed candle
int need = idx + 2;
if(CopyBuffer(g_BB_Handle, 1, 0, need, bbUpper) < need) return; // Upper
if(CopyBuffer(g_BB_Handle, 2, 0, need, bbLower) < need) return; // Lower
if(CopyBuffer(g_BB_Handle, 0, 0, need, bbMiddle) < need) return; // Middle
if(CopyBuffer(g_RSI_Handle, 0, 0, need, rsiVal) < need) return;
double closePrice = iClose(_Symbol, PERIOD_M1, idx);
double rsi = rsiVal[idx];
double bbUp = bbUpper[idx];
double bbLow = bbLower[idx];
// 11. M15 context
double contextMA = 0.0;
if(Inp_UseM15Context)
{
double maArr[];
ArraySetAsSeries(maArr, true);
if(CopyBuffer(g_MA_Handle, 0, 0, 2, maArr) < 2) return;
contextMA = maArr[0];
}
// 12. Signal generation
bool longSignal = false;
bool shortSignal = false;
// Long
if(Inp_AllowLong)
{
bool bbOk = !Inp_RequireOutsideBand || (closePrice < bbLow);
bool rsiOk = !Inp_RequireRSIFilter || (rsi < Inp_RSI_Oversold);
bool ctxOk = !Inp_UseM15Context || (closePrice > contextMA - Inp_ContextMATol);
longSignal = bbOk && rsiOk && ctxOk;
}
// Short
if(Inp_AllowShort)
{
bool bbOk = !Inp_RequireOutsideBand || (closePrice > bbUp);
bool rsiOk = !Inp_RequireRSIFilter || (rsi > Inp_RSI_Overbought);
bool ctxOk = !Inp_UseM15Context || (closePrice < contextMA + Inp_ContextMATol);
shortSignal = bbOk && rsiOk && ctxOk;
}
// 13. Position check
if(longSignal && Inp_UseOnePositionPerDir && HasPositionInDirection(POSITION_TYPE_BUY))
longSignal = false;
if(shortSignal && Inp_UseOnePositionPerDir && HasPositionInDirection(POSITION_TYPE_SELL))
shortSignal = false;
// 14. Execute
if(longSignal)
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = ask - Inp_StopLossPips * g_PipSize;
double tp = ask + Inp_TakeProfitPips * g_PipSize;
sl = NormalizeDouble(sl, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
tp = NormalizeDouble(tp, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
double lot = CalculateLot(Inp_StopLossPips);
OpenOrder(ORDER_TYPE_BUY, lot, ask, sl, tp);
}
else if(shortSignal)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = bid + Inp_StopLossPips * g_PipSize;
double tp = bid - Inp_TakeProfitPips * g_PipSize;
sl = NormalizeDouble(sl, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
tp = NormalizeDouble(tp, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
double lot = CalculateLot(Inp_StopLossPips);
OpenOrder(ORDER_TYPE_SELL, lot, bid, sl, tp);
}
// 15. Trailing stop management
if(Inp_UseTrailingStop) ManageTrailingStop();
}
//+------------------------------------------------------------------+
//| IsNewBar — returns true only once per M1 candle |
//+------------------------------------------------------------------+
bool IsNewBar()
{
datetime barTime = iTime(_Symbol, PERIOD_M1, 0);
if(barTime == g_LastBarTime) return false;
g_LastBarTime = barTime;
return true;
}
//+------------------------------------------------------------------+
//| IsTradeTime — returns true if current time is in session |
//+------------------------------------------------------------------+
bool IsTradeTime()
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
int nowMins = dt.hour * 60 + dt.min;
int startMin = Inp_StartHour * 60 + Inp_StartMinute;
int endMin = Inp_EndHour * 60 + Inp_EndMinute;
return (nowMins >= startMin && nowMins < endMin);
}
//+------------------------------------------------------------------+
//| GetCurrentSpreadPips |
//+------------------------------------------------------------------+
double GetCurrentSpreadPips()
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
return (ask - bid) / g_PipSize;
}
//+------------------------------------------------------------------+
//| LoadNewsFile — parse news.txt (format: "YYYY.MM.DD HH:MM") |
//+------------------------------------------------------------------+
void LoadNewsFile()
{
int fh = FileOpen(Inp_NewsFile, FILE_READ | FILE_TXT | FILE_COMMON);
if(fh == INVALID_HANDLE)
{
Print("News file '", Inp_NewsFile, "' not found — news filter skipped.");
return;
}
g_NewsCount = 0;
ArrayResize(g_NewsTimes, 0);
while(!FileIsEnding(fh))
{
string line = FileReadString(fh);
StringTrimRight(line);
StringTrimLeft(line);
if(StringLen(line) < 16) continue;
datetime t = StringToTime(line);
if(t > 0)
{
ArrayResize(g_NewsTimes, g_NewsCount + 1);
g_NewsTimes[g_NewsCount++] = t;
}
}
FileClose(fh);
Print("News filter loaded: ", g_NewsCount, " events from ", Inp_NewsFile);
}
//+------------------------------------------------------------------+
//| IsNewsTime — returns true if within news window |
//+------------------------------------------------------------------+
bool IsNewsTime()
{
if(g_NewsCount == 0) return false;
datetime now = TimeCurrent();
int windowSec = g_NewsMinutes * 60;
for(int i = 0; i < g_NewsCount; i++)
{
if(MathAbs((double)(now - g_NewsTimes[i])) <= windowSec)
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| CalculateLot — risk-based or fixed |
//+------------------------------------------------------------------+
double CalculateLot(int slPips)
{
if(Inp_UseFixedLot) return NormaliseLot(Inp_FixedLot);
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmt = balance * Inp_RiskPercent / 100.0;
double tickVal = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
// pip value per lot in account currency
double pipValuePerLot = (g_PipSize / tickSize) * tickVal;
if(pipValuePerLot <= 0) return NormaliseLot(Inp_FixedLot);
double rawLot = riskAmt / ((double)slPips * pipValuePerLot);
return NormaliseLot(rawLot);
}
//+------------------------------------------------------------------+
//| NormaliseLot — round to lot step, clamp to min/max |
//+------------------------------------------------------------------+
double NormaliseLot(double lot)
{
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double lotMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double lotMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
lot = MathFloor(lot / lotStep) * lotStep;
lot = MathMax(lot, lotMin);
lot = MathMin(lot, lotMax);
return NormalizeDouble(lot, 2);
}
//+------------------------------------------------------------------+
//| OpenOrder — send with retry loop |
//+------------------------------------------------------------------+
void OpenOrder(ENUM_ORDER_TYPE type, double lot, double price,
double sl, double tp)
{
for(int attempt = 1; attempt <= Inp_MaxRetries; attempt++)
{
bool sent = false;
if(type == ORDER_TYPE_BUY)
sent = g_Trade.Buy(lot, _Symbol, price, sl, tp, Inp_TradeComment);
else
sent = g_Trade.Sell(lot, _Symbol, price, sl, tp, Inp_TradeComment);
if(sent)
{
ulong ticket = g_Trade.ResultOrder();
string dir = (type == ORDER_TYPE_BUY) ? "BUY" : "SELL";
Print(TimeToString(TimeCurrent()), " | ORDER OPENED | ", dir,
" | Ticket=", ticket,
" | Lot=", DoubleToString(lot, 2),
" | Price=", DoubleToString(price, _Digits),
" | SL=", DoubleToString(sl, _Digits),
" | TP=", DoubleToString(tp, _Digits));
return;
}
int err = GetLastError();
Print("Order attempt ", attempt, " failed. Error=", err,
" | Retcode=", g_Trade.ResultRetcode());
// Don't retry on hard errors
if(err == ERR_MARKET_CLOSED || err == ERR_TRADE_DISABLED) break;
Sleep(500);
}
Print("Order FAILED after ", Inp_MaxRetries, " retries.");
}
//+------------------------------------------------------------------+
//| HasPositionInDirection |
//+------------------------------------------------------------------+
bool HasPositionInDirection(ENUM_POSITION_TYPE dir)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(g_Position.SelectByIndex(i))
{
if(g_Position.Magic() == Inp_MagicNumber &&
g_Position.Symbol() == _Symbol &&
g_Position.PositionType() == dir)
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
//| ManageTrailingStop |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
double trailStart = Inp_TrailingStartPips * g_PipSize;
double trailStep = Inp_TrailingStepPips * g_PipSize;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(!g_Position.SelectByIndex(i)) continue;
if(g_Position.Magic() != Inp_MagicNumber) continue;
if(g_Position.Symbol() != _Symbol) continue;
double sl = g_Position.StopLoss();
double openPx = g_Position.PriceOpen();
double digits = (double)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
if(g_Position.PositionType() == POSITION_TYPE_BUY)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double profit = bid - openPx;
if(profit >= trailStart)
{
double newSL = NormalizeDouble(bid - trailStep, (int)digits);
if(newSL > sl + _Point)
g_Trade.PositionModify(g_Position.Ticket(), newSL,
g_Position.TakeProfit());
}
}
else // SELL
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double profit = openPx - ask;
if(profit >= trailStart)
{
double newSL = NormalizeDouble(ask + trailStep, (int)digits);
if(newSL < sl - _Point || sl == 0)
g_Trade.PositionModify(g_Position.Ticket(), newSL,
g_Position.TakeProfit());
}
}
}
}
//+------------------------------------------------------------------+
//| CloseAllPositions |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(g_Position.SelectByIndex(i))
{
if(g_Position.Magic() == Inp_MagicNumber &&
g_Position.Symbol() == _Symbol)
{
g_Trade.PositionClose(g_Position.Ticket());
Print(TimeToString(TimeCurrent()),
" | EMERGENCY CLOSE | Ticket=", g_Position.Ticket(),
" | Reason: Drawdown limit");
g_LastTradeCloseTime = TimeCurrent();
}
}
}
}
//+------------------------------------------------------------------+
//| GetDayStart — midnight of given datetime |
//+------------------------------------------------------------------+
datetime GetDayStart(datetime t)
{
MqlDateTime dt;
TimeToStruct(t, dt);
dt.hour = 0; dt.min = 0; dt.sec = 0;
return StructToTime(dt);
}
//+------------------------------------------------------------------+
//| OnTradeTransaction — track close time for cooldown |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
{
if(trans.deal_type == DEAL_TYPE_BUY || trans.deal_type == DEAL_TYPE_SELL)
{
// Check if this deal closes a position
if((ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY)
== DEAL_ENTRY_OUT)
{
if((long)HistoryDealGetInteger(trans.deal, DEAL_MAGIC)
== Inp_MagicNumber)
{
double profit = HistoryDealGetDouble(trans.deal, DEAL_PROFIT);
Print(TimeToString(TimeCurrent()),
" | POSITION CLOSED | Deal=", trans.deal,
" | Profit=", DoubleToString(profit, 2));
g_LastTradeCloseTime = TimeCurrent();
}
}
}
}
}
//+------------------------------------------------------------------+
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env python3
"""
GENESIS Backtesting Engine v1
Uses yfinance for 1-year H1 historical data.
Runs the exact same EMA/RSI/ATR strategy as the live system.
Outputs performance report + sends results to Telegram.
"""
import os, json, requests
from datetime import datetime, timezone
from pathlib import Path
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# Symbol mapping: MT5 broker suffix → Yahoo Finance ticker
SYMBOL_MAP = {
"EURUSDxx": "EURUSD=X",
"XAUUSDxx": "GC=F",
"GBPUSDxx": "GBPUSD=X",
"GBPJPYxx": "GBPJPY=X",
"USDJPYxx": "USDJPY=X",
"EURUSD": "EURUSD=X",
"XAUUSD": "GC=F",
"GBPUSD": "GBPUSD=X",
"GBPJPY": "GBPJPY=X",
"USDJPY": "USDJPY=X",
}
def tg(msg):
try:
requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": msg, "parse_mode": "Markdown"}, timeout=15)
except: pass
def backtest_symbol(mt5_sym, yf_sym):
import yfinance as yf
import pandas as pd
import ta
print(f"\n{'='*50}")
print(f"Backtesting: {mt5_sym} ({yf_sym})")
df = yf.download(yf_sym, period="1y", interval="1h", progress=False, auto_adjust=True)
if df.empty or len(df) < 100:
print(f" Insufficient data: {len(df)} bars")
return None
# Flatten multi-index if present
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df.columns = [c.lower() for c in df.columns]
df = df.rename(columns={"adj close": "close"})
df = df.dropna()
# Calculate indicators using 'ta' instead of 'pandas-ta'
df["ema20"] = ta.trend.ema_indicator(df["close"], window=20)
df["ema50"] = ta.trend.ema_indicator(df["close"], window=50)
df["rsi"] = ta.momentum.rsi(df["close"], window=14)
df["atr"] = ta.volatility.average_true_range(df["high"], df["low"], df["close"], window=14)
df = df.dropna()
print(f" Downloaded {len(df)} H1 bars | {df.index[0].date()}{df.index[-1].date()}")
# Strategy: EMA20 > EMA50 + RSI < 45 → Buy | EMA20 < EMA50 + RSI > 55 → Sell
# SL = 2x ATR below/above entry | TP = 4x ATR (2:1 R:R minimum)
trades = []
in_trade = False
entry_price = sl = tp = direction = entry_idx = None
for i in range(1, len(df)):
row = df.iloc[i]
prev = df.iloc[i-1]
spread_est = row["atr"] * 0.05 # rough spread estimate
if not in_trade:
# Entry signals
if row["ema20"] > row["ema50"] and prev["rsi"] < 45 and row["rsi"] > 45:
direction = "Buy"
entry_price = row["close"] + spread_est
sl = round(entry_price - 2.0 * row["atr"], 5)
tp = round(entry_price + 4.0 * row["atr"], 5)
in_trade = True
entry_idx = i
elif row["ema20"] < row["ema50"] and prev["rsi"] > 55 and row["rsi"] < 55:
direction = "Sell"
entry_price = row["close"] - spread_est
sl = round(entry_price + 2.0 * row["atr"], 5)
tp = round(entry_price - 4.0 * row["atr"], 5)
in_trade = True
entry_idx = i
else:
# Check SL/TP hit
high, low = row["high"], row["low"]
result = None
if direction == "Buy":
if low <= sl:
result = "loss"; exit_price = sl
elif high >= tp:
result = "win"; exit_price = tp
else:
if high >= sl:
result = "loss"; exit_price = sl
elif low <= tp:
result = "win"; exit_price = tp
# Max hold: 48 bars (2 days)
if result is None and (i - entry_idx) >= 48:
result = "timeout"; exit_price = row["close"]
if result:
diff = (exit_price - entry_price) if direction == "Buy" else (entry_price - exit_price)
if "JPY" in mt5_sym:
pips = round(diff * 100.0, 1)
elif "XAU" in mt5_sym or "GC" in yf_sym:
pips = round(diff, 2)
else:
pips = round(diff * 10000.0, 1)
trades.append({
"direction": direction,
"entry": entry_price,
"exit": exit_price,
"result": result,
"pips": pips,
"bars_held": i - entry_idx,
"date": df.index[entry_idx].strftime("%Y-%m-%d"),
})
in_trade = False
if not trades:
print(" No trades generated")
return None
wins = [t for t in trades if t["result"] == "win"]
losses = [t for t in trades if t["result"] == "loss"]
timeouts= [t for t in trades if t["result"] == "timeout"]
total_pips = sum(t["pips"] for t in trades)
win_pips = sum(t["pips"] for t in wins)
loss_pips = sum(t["pips"] for t in losses)
winrate = len(wins) / len(trades) * 100
# Profit factor
pf = round(abs(win_pips / loss_pips), 2) if loss_pips != 0 else float("inf")
# Max drawdown (running pip balance)
running = 0; peak = 0; max_dd = 0
for t in trades:
running += t["pips"]
if running > peak: peak = running
dd = peak - running
if dd > max_dd: max_dd = dd
result = {
"symbol": mt5_sym,
"yf": yf_sym,
"total_trades": len(trades),
"wins": len(wins),
"losses": len(losses),
"timeouts": len(timeouts),
"win_rate": round(winrate, 1),
"total_pips": round(total_pips, 1),
"profit_factor": pf,
"max_drawdown_pips": round(max_dd, 1),
"avg_hold_bars": round(sum(t["bars_held"] for t in trades) / len(trades), 1),
}
print(f" Trades: {result['total_trades']} | W:{result['wins']} L:{result['losses']} T:{result['timeouts']}")
print(f" Win rate: {result['win_rate']}% | Total pips: {result['total_pips']}")
print(f" Profit factor: {result['profit_factor']} | Max DD: {result['max_drawdown_pips']} pips")
return result
def main():
tg("🔬 *GENESIS Backtest Starting*\nRunning 1-year H1 backtest on 5 symbols using EMA20/50 + RSI + ATR strategy...\n_This will take ~60 seconds._")
results = []
for mt5_sym, yf_sym in SYMBOL_MAP.items():
try:
r = backtest_symbol(mt5_sym, yf_sym)
if r:
results.append(r)
except Exception as e:
print(f" ERROR {mt5_sym}: {e}")
if not results:
tg("❌ *Backtest Failed*: No results generated.")
return
# Save results
try:
out_path = Path("/var/log/hermes/backtest_results.json")
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(results, indent=2))
print(f"\nResults saved to {out_path}")
except Exception as e:
print(f"\nCould not write to /var/log/hermes/backtest_results.json ({e}). Falling back to local workspace.")
out_path = Path("./backtest_results.json")
out_path.write_text(json.dumps(results, indent=2))
print(f"Results saved to {out_path.resolve()}")
# Build Telegram report
report = "📊 *GENESIS Backtest Results* (1 Year H1)\n"
report += "Strategy: EMA20/50 crossover + RSI + 2x ATR SL + 4x ATR TP\n\n"
overall_trades = sum(r["total_trades"] for r in results)
overall_wins = sum(r["wins"] for r in results)
overall_wr = round(overall_wins / overall_trades * 100, 1) if overall_trades else 0
for r in sorted(results, key=lambda x: x["win_rate"], reverse=True):
emoji = "" if r["win_rate"] >= 50 and r["profit_factor"] >= 1.0 else "⚠️" if r["win_rate"] >= 45 else ""
report += f"{emoji} *{r['symbol']}*\n"
report += f" {r['wins']}W/{r['losses']}L | WR: {r['win_rate']}% | PF: {r['profit_factor']}\n"
report += f" Pips: {r['total_pips']} | Max DD: {r['max_drawdown_pips']} pips\n\n"
report += f"📈 *Overall:* {overall_wins}/{overall_trades} trades won ({overall_wr}%)\n"
# Strategy verdict
viable = [r for r in results if r["win_rate"] >= 50 and r["profit_factor"] >= 1.2]
if viable:
report += f"\n✅ *Viable symbols*: {', '.join(r['symbol'] for r in viable)}\n"
report += "_These pairs have >50% win rate and >1.2 profit factor historically._"
else:
report += "\n⚠️ *No symbol meets viability criteria (>50% WR + >1.2 PF)*\n"
report += "_Strategy needs tuning before live deployment._"
print("\n" + report)
tg(report)
# Save markdown report
md = f"# GENESIS Backtest Report\n*Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')} UTC*\n\n"
md += report.replace("*", "**").replace("_", "*")
try:
report_path = Path("/var/log/hermes/backtest_report.md")
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(md)
print(f"Report saved to {report_path}")
except Exception as e:
print(f"Could not write to /var/log/hermes/backtest_report.md ({e}). Falling back to local workspace.")
report_path = Path("./backtest_report.md")
report_path.write_text(md)
print(f"Report saved to {report_path.resolve()}")
if __name__ == "__main__":
main()
+64
View File
@@ -0,0 +1,64 @@
# GENESIS — Apollo Strategy C Configuration
# MA Crossover Trend Following — runs alongside Hermes + Ares without interference
bridge:
url: "http://127.0.0.1:8000"
timeout_seconds: 15
mt5_api:
url: "MT5_BRIDGE_URL env var"
api_key: "MT5_BRIDGE_KEY env var"
telegram:
chat_id: "YOUR_TELEGRAM_CHAT_ID"
# Symbols Apollo watches (trend following works best on trending pairs)
symbols:
- "EURUSDxx"
- "GBPUSDxx"
- "USDJPYxx"
- "XAUUSDxx"
- "GBPJPYxx"
# MA Crossover parameters
indicators:
fast_ma_period: 9 # Fast MA — golden/death cross
slow_ma_period: 21 # Slow MA
ma_method: "EMA" # EMA or SMA
signal_timeframe: "M5" # M5 for entries
trend_timeframe: "H1" # H1 for trend direction filter
trend_ma_period: 50 # H1 SMA50 — only trade in trend direction
atr_period: 14 # ATR for dynamic SL
risk:
risk_pct: 0.01 # 1% risk per trade
min_rr_ratio: 1.5 # Minimum R:R
sl_atr_multiplier: 1.5 # SL = ATR × this value
tp_atr_multiplier: 3.0 # TP = ATR × this value
max_spread_pips: 1.5
block_news_minutes: 45 # Slightly less strict than Ares (trend can absorb news)
block_medium_news: false
strictness:
# Trend following is MORE patient — requires H1 trend alignment
require_trend_alignment: true # H1 MA must agree with signal direction
min_ma_separation_pct: 0.001 # MAs must be at least 0.1% apart to confirm crossover
min_adx: 20 # Minimum trend strength (lower than Ares — trend needs less extreme)
cooldown_seconds: 120 # 2min cooldown between signals (prevent whipsaw)
sessions:
# Trend following works best during high-liquidity sessions
allowed:
- {start: 7, end: 20} # London + NY
journal:
path: "/var/log/apollo/trade_journal.jsonl"
cache:
path: "/tmp/genesis_cache.json" # Shared with Hermes + Ares
strategy:
name: "Apollo"
version: "1.0"
comment: "APOLLO-v1" # Distinguishes from GENESIS-v2 and ARES-v1
magic_number: 20250515
+51
View File
@@ -0,0 +1,51 @@
# GENESIS — Strategy B (Ares) Configuration
# Controls ONLY Strategy B. Strategy A (Hermes) is completely untouched.
bridge:
url: "http://127.0.0.1:8000"
timeout_seconds: 15
mt5_api:
url: "MT5_BRIDGE_URL env var"
api_key: "MT5_BRIDGE_KEY env var"
telegram:
chat_id: "YOUR_TELEGRAM_CHAT_ID"
symbols:
- "EURUSDxx"
- "XAUUSDxx"
- "GBPUSDxx"
risk:
risk_pct: 0.01 # 1% risk per trade
max_simultaneous: 1 # Max 1 open position
min_rr_ratio: 1.5 # Minimum R:R
block_news_minutes: 60 # Block X minutes before high-impact events
block_medium_news: true # Also block medium-impact
# BB+RSI EA pip settings (mirrors MQL5 EA inputs)
sl_pips: 20
tp_pips: 40
strictness:
min_confluence_count: 3 # N conditions must align before signalling
required_timeframes: ["H1", "H4"] # All must agree on direction
min_adx: 22
rsi_oversold: 30
rsi_overbought: 70
sessions:
allowed:
- {start: 7, end: 21} # London + NY UTC hours
journal:
path: "/var/log/ares/trade_journal.jsonl"
cache:
path: "/tmp/genesis_cache.json" # Shared with Hermes — zero extra API calls
strategy:
name: "Ares"
version: "1.0"
comment: "ARES-v1" # MT5 order comment — distinguishes from GENESIS-v2
+75
View File
@@ -0,0 +1,75 @@
# GENESIS — Artemis Strategy E Configuration
# Ichimoku Kumo Breakout — H1 timeframe, high-quality low-frequency signals
# Complements: Ares (BB+RSI M1), Apollo (MA Cross M5), Athena (BB+RSI M5)
bridge:
url: "http://127.0.0.1:8000"
timeout_seconds: 15
mt5_api:
url: "MT5_BRIDGE_URL env var"
api_key: "MT5_BRIDGE_KEY env var"
telegram:
chat_id: "YOUR_TELEGRAM_CHAT_ID"
symbols:
- "EURUSDxx"
- "GBPJPYxx" # Ichimoku originated in Japan — JPY pairs are ideal
- "GBPUSDxx"
- "USDJPYxx"
- "XAUUSDxx"
ichimoku:
tenkan_period: 9 # Conversion Line — short-term trend
kijun_period: 26 # Base Line — medium-term trend / SL reference
senkou_b_period: 52 # Slow cloud boundary
displacement: 26 # Kumo is plotted 26 bars AHEAD of price
# Signal quality gates
require_cloud_color_alignment: true # Buy only if future cloud is green (SpanA > SpanB)
confirmation_bars: 1 # Must close outside Kumo for N full bars
require_chikou_confirmation: true # Chikou Span must be above/below price
require_price_above_kijun: true # Price above Kijun for buys, below for sells
confirmation:
use_rsi: true
rsi_period: 14
rsi_buy_threshold: 50 # RSI > 50 for buys
rsi_sell_threshold: 50 # RSI < 50 for sells
use_awesome_oscillator: false # AO is optional — disabled by default (RSI is enough)
ao_fast: 5
ao_slow: 34
risk:
risk_pct: 0.0075 # 0.75% per trade — higher quality signal justifies larger size
min_rr_ratio: 2.0 # H1 signals justify higher R:R requirement
sl_kijun_buffer: 0.0002 # SL placed just below/above Kijun-sen + buffer
use_kumo_sl: true # Alternative: SL at near edge of Kumo
tp_multiplier: 2.5 # TP = SL distance × 2.5
max_spread_pips: 2.0 # H1 allows wider spread tolerance
block_news_minutes: 60
block_medium_news: false
strictness:
cooldown_seconds: 300 # 5 min cooldown — H1 signals are rare, no need to spam
signal_timeframe: "H1"
trend_timeframe: "D1" # Daily trend context (optional double-check)
sessions:
# Ichimoku works best during liquid sessions
allowed:
- {start: 7, end: 21}
journal:
path: "/var/log/artemis/trade_journal.jsonl"
cache:
path: "/tmp/genesis_cache.json"
strategy:
name: "Artemis"
version: "1.0"
comment: "ARTEMIS-v1" # Unique tag — 5th strategy
magic_number: 20250517
+68
View File
@@ -0,0 +1,68 @@
# GENESIS — Athena Strategy D Configuration
# BB+RSI Mean Reversion on M5 — faster signals than Ares (M1 strict)
# Complements: Ares (M1 strict), Apollo (MA Trend), Hermes (free AI)
bridge:
url: "http://127.0.0.1:8000"
timeout_seconds: 15
mt5_api:
url: "MT5_BRIDGE_URL env var"
api_key: "MT5_BRIDGE_KEY env var"
telegram:
chat_id: "YOUR_TELEGRAM_CHAT_ID"
symbols:
- "EURUSDxx"
- "GBPUSDxx"
- "XAUUSDxx"
- "USDJPYxx"
- "GBPJPYxx"
indicators:
bb_period: 20
bb_deviation: 2.0
bb_ma_method: "SMA"
rsi_period: 14
rsi_oversold: 30
rsi_overbought: 70
signal_timeframe: "M5" # M5 — faster than Ares (M1), captures intraday moves
trend_timeframe: "H4" # H4 context filter — broader than Ares (M15)
trend_ma_period: 50
atr_period: 14
volume_ma_period: 20 # Volume confirmation (OBV direction)
risk:
risk_pct: 0.005 # 0.5% — half of Ares/Apollo (more signals, smaller size)
min_rr_ratio: 1.5
sl_atr_multiplier: 1.2 # Tighter SL than Ares (M5 ATR is smaller)
tp_atr_multiplier: 2.5 # TP slightly lower (M5 moves less than M1 extremes)
max_spread_pips: 1.5
block_news_minutes: 30 # Less strict than Ares (M5 recovers faster)
block_medium_news: false
strictness:
# Athena is SIMPLER than Ares — only 2 hard conditions (BB + RSI)
# ADX is a soft bonus, not a gate — allows trading in quieter markets too
require_band_close: true # Candle must CLOSE outside BB (not just wick)
soft_adx_bonus: true # ADX > 18 adds confidence but doesn't block
require_h4_context: true # H4 SMA50 must agree with direction
cooldown_seconds: 90 # 90s between signals per symbol
max_signals_per_hour: 4 # Rate limit — prevents overtrading on choppy M5
sessions:
allowed:
- {start: 6, end: 20} # Slightly wider than Ares
journal:
path: "/var/log/athena/trade_journal.jsonl"
cache:
path: "/tmp/genesis_cache.json"
strategy:
name: "Athena"
version: "1.0"
comment: "ATHENA-v1" # Unique tag — distinguishes from GENESIS-v2, ARES-v1, APOLLO-v1
magic_number: 20250516
+51
View File
@@ -0,0 +1,51 @@
# GENESIS — Hephaestus Strategy F Configuration
# Grid + Martingale — EXTREME RISK — circuit breakers MANDATORY
# confirm_risk_acknowledged MUST be true before strategy runs
strategy:
name: "Hephaestus"
version: "1.0"
comment: "HEPH-v1"
magic_number: 20250518
confirm_risk_acknowledged: true # SET TO true ONLY AFTER READING WARNING
bridge:
url: "http://127.0.0.1:8000"
timeout_seconds: 15
mt5_api:
url: "MT5_BRIDGE_URL env var"
api_key: "MT5_BRIDGE_KEY env var"
telegram:
chat_id: "YOUR_TELEGRAM_CHAT_ID"
symbol: "EURUSDxx" # Single pair only — never multi-pair for grid
direction: "both" # "buy_only", "sell_only", "both"
grid:
initial_lot: 0.01 # START TINY — martingale compounds fast
martingale_multiplier: 1.5 # 1.5x safer than 2x. 2x will blow account in 6 levels
max_lot_per_order: 0.20 # Hard cap — never exceeded regardless of martingale
grid_spacing_pips: 20 # Distance between grid levels
max_grid_levels: 4 # HARD LIMIT — level 4 = 0.01 × 1.5³ = 0.034 lot
take_profit_pips: 15 # TP per individual trade
basket_tp_pips: 25 # Close all if total basket profit ≥ this
circuit_breakers:
max_equity_drawdown_pct: 8.0 # Kill all if equity drops 8% from peak
max_daily_loss_pct: 4.0 # Stop for day if daily loss ≥ 4%
max_consecutive_losses: 4 # Reset grid after 4 consecutive losing levels
cooldown_after_reset_sec: 600 # 10 min cooldown before restarting grid
max_spread_pips: 1.5 # No new levels if spread too wide
max_total_lots: 0.5 # Total exposure cap across all grid levels
sessions:
allowed:
- {start: 7, end: 20} # Only trade during liquid hours
journal:
path: "/var/log/hephaestus/trade_journal.jsonl"
cache:
path: "/tmp/genesis_cache.json"
+92
View File
@@ -0,0 +1,92 @@
# GENESIS — Zeus Strategy G Configuration
# ICT Smart Money: Liquidity Sweep + FVG + Order Block on M5
# Three-layer sequential confirmation: Sweep → FVG → OB
bridge:
url: "http://127.0.0.1:8000"
timeout_seconds: 15
mt5_api:
url: "MT5_BRIDGE_URL env var"
api_key: "MT5_BRIDGE_KEY env var"
telegram:
chat_id: "YOUR_TELEGRAM_CHAT_ID"
symbols:
- "EURUSDxx"
- "GBPUSDxx"
- "GBPJPYxx"
- "XAUUSDxx"
ict:
entry_timeframe: "M5" # Entry signals
context_timeframe: "M15" # Structure analysis
liquidity:
swing_lookback: 5 # Bars each side for pivot detection
sweep_tolerance: 0.0003 # Price must exceed level by this to count
require_rejection: true # Close must return inside structure (rejection wick)
session_highs_lows: true # Also detect previous session H/L sweeps
fvg:
min_gap_pips: 1.5 # Minimum FVG size to count
max_age_bars: 10 # Ignore FVGs older than N bars
require_unmitigated: true # FVG must not have been filled yet
order_block:
max_age_bars: 15 # Stale OBs ignored
min_body_ratio: 0.35 # Candle body must be ≥35% of full range
max_wick_ratio: 0.40 # Total wick ≤40% of full range
structure:
swing_lookback: 5
bos_bars: 2 # Bars to confirm structure break
confluence:
min_score: 65 # 0-100 — minimum to generate signal
max_daily_trades: 2 # ICT is low frequency — 5-15 signals/month
allow_third_if_score_ge: 90
killzone:
enabled: true
london: {start: 8, end: 11} # GMT hours
ny: {start: 13, end: 16}
scoring:
bos_strength: 20
sweep_quality: 15
fvg_presence: 15
ob_quality: 20
killzone: 10
mtf_confluence: 10
ob_freshness: 10
risk:
risk_pct: 0.0075 # 0.75% per trade
min_rr: 2.0
sl_ob_buffer: 0.0002 # SL placed just below/above OB + buffer
tp_multiplier: 2.5
max_spread_pips: 2.0
cooldown_seconds: 120
circuit_breakers:
max_equity_drawdown_pct: 15.0
max_daily_loss_pct: 6.0
max_consecutive_losses: 4
sessions:
allowed:
- {start: 7, end: 21}
journal:
path: "/var/log/zeus/trade_journal.jsonl"
cache:
path: "/tmp/genesis_cache.json"
strategy:
name: "Zeus"
version: "1.0"
comment: "ZEUS-v1"
magic_number: 20250519
+257
View File
@@ -0,0 +1,257 @@
#!/usr/bin/env python3
"""
GENESIS Autonomous Strategy Execution Engine
Runs every 5 minutes. Scans all strategies, executes signals, manages risk.
No AI/LLM cost — the strategies decide everything algorithmically.
Uses Mt5Bridge REST API — no API2TRADE required.
"""
import json, subprocess, logging, requests, os, sys
from datetime import datetime, timezone
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent.parent / ".env")
# ── Config ────────────────────────────────────────────────────────────────────
ROOT = Path(__file__).parent.parent
AGENT = os.getenv("GENESIS_AGENT_DIR", str(ROOT))
PY = os.getenv("GENESIS_PYTHON", sys.executable)
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
CHAT = os.getenv("TELEGRAM_CHAT_ID", "")
LOG_DIR = Path(os.getenv("GENESIS_LOG_DIR", "/var/log/hermes"))
LOG_DIR.mkdir(parents=True, exist_ok=True)
LOG = LOG_DIR / "autonomous.log"
SNAP = LOG_DIR / "position_snapshot.json"
MAX_TOTAL_POSITIONS = int(os.getenv("MAX_POSITIONS", 4))
MAX_PER_STRATEGY = 1
logging.basicConfig(
filename=str(LOG), level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger("genesis")
# ── Mt5Bridge (unified adapter) ────────────────────────────────────────────────
sys.path.insert(0, str(Path(__file__).parent))
from mt5_bridge import bridge as _bridge
# ── Strategies: (name, short, symbols_to_scan) ───────────────────────────────
STRATEGIES = [
("ARES", "ares", ["EURUSDxx", "GBPUSDxx"]),
("APOLLO", "apollo", ["EURUSDxx", "GBPUSDxx", "XAUUSDxx"]),
("ATHENA", "athena", ["EURUSDxx", "GBPUSDxx"]),
("ARTEMIS", "artemis", ["EURUSDxx", "GBPUSDxx", "GBPJPYxx"]),
("ZEUS", "zeus", ["EURUSDxx", "GBPUSDxx", "XAUUSDxx"]),
]
def tg(msg):
"""Send Telegram message."""
if not TOKEN: return
try:
requests.post(f"https://api.telegram.org/bot{TOKEN}/sendMessage",
json={"chat_id": CHAT, "text": msg, "parse_mode": "Markdown"},
timeout=10)
except Exception as e:
log.warning(f"Telegram failed: {e}")
def api(endpoint, method="GET", data=None):
"""Mt5Bridge unified API call."""
return _bridge(endpoint, method, data)
def run_tool(tool, cmd, sym=""):
"""Run a strategy tool as subprocess. Path: strategies/{tool}/{tool}_tool.py"""
tool_path = f"{AGENT}/strategies/{tool}/{tool}_tool.py"
args = [PY, tool_path, cmd]
if sym: args.append(sym)
try:
r = subprocess.run(args, capture_output=True, text=True, timeout=50,
cwd=f"{AGENT}/strategies/{tool}")
out = r.stdout.strip()
return json.loads(out) if out else {"error": r.stderr.strip()[:120]}
except Exception as e:
return {"error": str(e)[:80]}
def pip_val(sym):
if "JPY" in sym: return 0.01
if "XAU" in sym: return 0.1
return 0.0001
def already_has_position(open_pos, strategy_tag):
"""Check if a strategy already has an open position."""
return any(strategy_tag.lower() in str(p.get("comment","")).lower() for p in open_pos)
def execute_signal(tool, sym, result, open_pos, balance):
"""Execute a trade signal with risk validation."""
direction = result.get("direction", "")
sl = result.get("stop_loss")
tp = result.get("take_profit")
vol = result.get("volume", 0.1)
if not all([direction, sl, tp, vol]):
log.warning(f"{tool}/{sym}: Signal missing fields: {result}")
return False
# Risk check: SL distance reasonable?
q = api(f"/quote?symbol={sym}")
price = float(q.get("ask" if direction=="Buy" else "bid", 0))
if price <= 0:
log.warning(f"{tool}/{sym}: Cannot get quote")
return False
pip = pip_val(sym)
sl_pips = abs(price - float(sl)) / pip
if sl_pips > 150:
log.warning(f"{tool}/{sym}: SL too wide ({sl_pips:.0f} pips), skipping")
return False
if sl_pips < 3:
log.warning(f"{tool}/{sym}: SL too tight ({sl_pips:.0f} pips), skipping")
return False
# Execute
order = api("/market", "POST", {
"symbol": sym,
"volume": vol,
"type": direction,
"stop_loss": float(sl),
"take_profit": float(tp),
"comment": f"{tool.upper()}-v1"
})
ticket = order.get("ticket") or order.get("Ticket")
if ticket:
rr = result.get("rr_ratio", "?")
msg = (
f"🟢 *TRADE OPENED*\n"
f"Strategy: {tool.upper()}\n"
f"{sym} {direction} {vol}lot\n"
f"Entry: {price:.5f} | SL: {sl} | TP: {tp}\n"
f"R:R = {rr} | SL = {sl_pips:.0f}pips\n"
f"Balance: €{balance:,.2f}"
)
tg(msg)
log.info(f"OPENED: {tool}/{sym} {direction} {vol}lot ticket={ticket} SL={sl} TP={tp}")
return True
else:
err = order.get("message", str(order))[:100]
log.error(f"Order failed {tool}/{sym}: {err}")
return False
def scan_and_execute():
now = datetime.now(timezone.utc)
log.info(f"=== Autonomous cycle {now.strftime('%Y-%m-%d %H:%M')} UTC ===")
# Account state
acc = api("/balance")
pos_data = api("/positions")
balance = float(acc.get("balance", 0))
equity = float(acc.get("equity", 0))
open_pos = pos_data if isinstance(pos_data, list) else []
n_open = len(open_pos)
log.info(f"Balance=€{balance:.2f} Equity=€{equity:.2f} OpenPositions={n_open}")
if n_open >= MAX_TOTAL_POSITIONS:
log.info(f"Max positions reached ({n_open}/{MAX_TOTAL_POSITIONS}). Skipping scans.")
return
# Position snapshot for trade-monitor (detect closes)
curr_snap = {str(p.get("ticket")): p for p in open_pos}
prev_snap = {}
if SNAP.exists():
try: prev_snap = json.loads(SNAP.read_text())
except: pass
# Detect closed positions and notify
for ticket, p in prev_snap.items():
if ticket not in curr_snap:
hist = api("/history")
pnl = None
if isinstance(hist, list):
for h in reversed(hist):
if str(h.get("ticket")) == ticket:
pnl = float(h.get("profit", h.get("pnl", 0)))
break
icon = "🟢" if (pnl or 0) >= 0 else "🔴"
tg(f"{icon} *TRADE CLOSED*\n"
f"{p.get('symbol')} {p.get('orderType','').upper()} "
f"{p.get('lots')}lot [{p.get('comment')}]\n"
f"Result: €{pnl:+.2f}" if pnl is not None else "Result: see MT5")
log.info(f"CLOSED: ticket={ticket} {p.get('symbol')} pnl={pnl}")
SNAP.parent.mkdir(parents=True, exist_ok=True)
SNAP.write_text(json.dumps(curr_snap))
# Slots available
slots = MAX_TOTAL_POSITIONS - n_open
log.info(f"Available slots: {slots}")
# Parallel scans
scan_tasks = []
for strat_name, tool, symbols in STRATEGIES:
if already_has_position(open_pos, strat_name):
log.info(f"{strat_name}: position already open, skipping scan")
continue
for sym in symbols:
scan_tasks.append((strat_name, tool, sym))
if not scan_tasks:
log.info("No scan tasks — all strategies have open positions")
return
results = {}
with ThreadPoolExecutor(max_workers=8) as ex:
futures = {ex.submit(run_tool, tool, "analyze", sym): (strat, sym)
for strat, tool, sym in scan_tasks}
for fut in as_completed(futures, timeout=60):
strat, sym = futures[fut]
try:
r = fut.result(timeout=1)
key = f"{strat}/{sym}"
results[key] = r
action = r.get("action", "?")
reason = r.get("reason", "")[:70]
log.info(f" {key}: {action} | {reason}")
except Exception as e:
log.warning(f" Scan error: {e}")
# Find signals
signals = [(k, v) for k, v in results.items() if v.get("action") == "trade"]
log.info(f"Signals found: {len(signals)}")
# Execute best signals (up to available slots)
executed = 0
for key, result in signals:
if executed >= slots:
break
strat_name, sym = key.split("/", 1)
tool = strat_name.lower()
# Double-check position not opened by another signal in this cycle
if already_has_position(open_pos, strat_name):
continue
log.info(f"Executing: {key} {result.get('direction')}")
success = execute_signal(tool, sym, result, open_pos, balance)
if success:
executed += 1
# Refresh positions so next iteration sees the new position
pos_data = api("/positions")
open_pos = pos_data if isinstance(pos_data, list) else open_pos
if executed == 0 and not signals:
log.info("No signals this cycle — all strategies waiting")
# Run Hephaestus grid tick
heph_r = run_tool("hephaestus", "tick") # path: strategies/hephaestus/hephaestus_tool.py
log.info(f"Hephaestus tick: {str(heph_r)[:80]}")
log.info(f"=== Cycle complete. Executed {executed} trade(s) ===")
if __name__ == "__main__":
try:
scan_and_execute()
except Exception as e:
log.error(f"CRASH: {e}", exc_info=True)
tg(f"🚨 *GENESIS AUTONOMOUS CRASH*: {str(e)[:200]}")
+311
View File
@@ -0,0 +1,311 @@
#!/usr/bin/env python3
"""
GENESIS Brain Feed — Hourly Telegram Broadcast
Every 60 minutes: scans all strategies in parallel, reports open trades,
P&L, what each strategy is seeing, and the outlook for the next hour.
"""
import os, json, time, logging, threading, sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError
import subprocess, requests, yaml
sys.path.insert(0, str(Path(__file__).parent))
from mt5_bridge import bridge as _bridge
# ── Config ─────────────────────────────────────────────────────────────────────
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
AGENT = "/opt/hermes-agent"
PYTHON = f"{AGENT}/.venv-hermes/bin/python3"
SCAN_TIMEOUT = 55 # seconds per strategy scan
JOURNALS = {
"GENESIS-v2": "/var/log/hermes/trade_journal.jsonl",
"ARES-v1": "/var/log/ares/trade_journal.jsonl",
"APOLLO-v1": "/var/log/apollo/trade_journal.jsonl",
"ATHENA-v1": "/var/log/athena/trade_journal.jsonl",
"ARTEMIS-v1": "/var/log/artemis/trade_journal.jsonl",
"HEPH-v1": "/var/log/hephaestus/trade_journal.jsonl",
"ZEUS-v1": "/var/log/zeus/trade_journal.jsonl",
}
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
def tg(msg: str):
"""Send message, splitting if >4000 chars."""
for chunk in [msg[i:i+4000] for i in range(0, len(msg), 4000)]:
try:
requests.post(f"https://api.telegram.org/bot{TOKEN}/sendMessage",
json={"chat_id": CHAT_ID, "text": chunk, "parse_mode": "Markdown"},
timeout=10)
time.sleep(0.3)
except Exception as e:
log.error(f"tg send error: {e}")
def bridge(path) -> dict:
return _bridge(path)
def run_tool(name: str, tool_file: str, cmd: str, symbol: str = "") -> dict:
"""Run a strategy tool and return parsed JSON result with timeout."""
args = [PYTHON, f"{AGENT}/{tool_file}", cmd]
if symbol: args.append(symbol)
try:
result = subprocess.run(args, capture_output=True, text=True,
timeout=SCAN_TIMEOUT, cwd=AGENT)
if result.stdout.strip():
return json.loads(result.stdout.strip())
return {"error": result.stderr.strip()[:100] or "No output"}
except subprocess.TimeoutExpired:
return {"error": "timeout"}
except Exception as e:
return {"error": str(e)[:100]}
def journal_summary(path: str, hours: int = 1) -> dict:
"""Summarize trades in the last N hours from a journal file."""
p = Path(path)
if not p.exists(): return {"trades": 0, "wins": 0, "losses": 0, "pnl": 0.0, "open": 0}
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
trades = wins = losses = open_t = 0
pnl = 0.0
try:
for line in p.read_text().strip().split("\n"):
if not line: continue
try:
t = json.loads(line)
opened = t.get("opened","")
if opened:
try:
dt = datetime.fromisoformat(opened.replace("Z","+00:00"))
if dt < cutoff: continue
except: continue
trades += 1
result = t.get("result")
p_val = float(t.get("pnl") or 0)
if result == "win": wins += 1; pnl += p_val
elif result == "loss": losses += 1; pnl += p_val
elif result is None: open_t += 1
except: continue
except: pass
return {"trades": trades, "wins": wins, "losses": losses, "pnl": round(pnl,2), "open": open_t}
def icon(action):
return "🟢" if action == "trade" else ""
def fmt_strategy_result(name: str, result: dict) -> str:
action = result.get("action","wait")
if action == "trade":
return (f"{icon(action)} *{name}*: SIGNAL {result.get('direction','?')} "
f"`{result.get('symbol','?')}` "
f"R:R {result.get('rr_ratio','?')} | "
f"Vol {result.get('volume','?')} | "
f"Score {result.get('confidence_score',result.get('confidence','?'))}")
reason = result.get("reason","")[:80]
layer = result.get("layer","")
prog = f" [{layer}]" if layer else ""
return f"{icon(action)} *{name}*: WAIT{prog}{reason}"
def fmt_heph_status(result: dict) -> str:
if "error" in result:
return f"⚙️ *HEPHAESTUS*: {result['error'][:80]}"
enabled = result.get("enabled", False)
bl = result.get("buy_level",0); sl = result.get("sell_level",0)
lots = result.get("total_lots",0); pnl = result.get("unrealized_pnl",0)
status = "RUNNING" if enabled else f"DISABLED — {result.get('killed_reason','?')}"
return (f"⚙️ *HEPHAESTUS*: {status} | "
f"Buy L{bl} Sell L{sl} | {lots}lots | PnL €{pnl:.2f}")
def estimate_next_hour(scan_results: dict) -> list[str]:
"""Generate outlook text based on current scan results + session timing."""
now_utc = datetime.now(timezone.utc)
hr = now_utc.hour + now_utc.minute/60
weekday = now_utc.weekday() # 0=Mon 4=Fri 5=Sat 6=Sun
signals = [(n,r) for n,r in scan_results.items() if r.get("action")=="trade"]
waits = [(n,r) for n,r in scan_results.items() if r.get("action")!="trade"]
lines = []
# ── Session context ────────────────────────────────────────────
is_weekend = weekday >= 5
in_asia = 0 <= hr < 5
in_london = 7 <= hr < 11
in_ny = 12 <= hr < 17
in_session = not is_weekend and (5 <= hr < 21)
if is_weekend:
opens_in = (6 - weekday) * 24 - hr + 22 # hours until Mon 22:00 UTC
lines.append(f"🌙 *Weekend* — markets closed.")
lines.append(f" Forex opens ~{round(opens_in,1)}h from now (Mon 22:00 UTC)")
lines.append(f" Hermes resumes scanning at session open.")
elif in_london:
lines.append("🏦 *London session active* (07:0011:00 UTC)")
lines.append(" Zeus + Apollo most active here. Expect 13 scan cycles.")
elif in_ny:
lines.append("🗽 *New York session active* (12:0017:00 UTC)")
lines.append(" Zeus + Apollo most active. Ares/Athena also scanning.")
elif in_asia:
lines.append("🌏 *Asian session* (00:0005:00 UTC) — lower volatility")
lines.append(" Ares + Athena may fire on GBPJPY/XAUUSD ranging moves.")
elif not in_session:
next_open = 5 - hr if hr < 5 else 29 - hr # next 05:00 UTC
lines.append(f"🌙 *Off-hours* — strategies paused.")
lines.append(f" Next session opens in ~{abs(round(next_open,1))}h (05:00 UTC)")
lines.append(f" London killzone in ~{round(max(0,7-hr),1)}h — Zeus high-probability window")
lines.append("")
# ── Live signals ───────────────────────────────────────────────
if signals:
lines.append(f"🔥 *{len(signals)} live signal(s) ready:*")
for name, r in signals:
lines.append(f"{name}: {r.get('direction')} `{r.get('symbol')}` "
f"— Hermes will evaluate for execution")
# ── Almost-ready signals ───────────────────────────────────────
for name, r in waits:
reason = r.get("reason","")
layer = r.get("layer","")
score = r.get("score", r.get("confidence_score", 0)) or 0
if "cooldown" in reason.lower():
lines.append(f"{name}: cooldown — resets shortly")
elif "2/3" in layer or "3/3" in layer:
lines.append(f"🔶 {name}: *{layer}* — one more confirmation needed")
elif isinstance(score, (int, float)) and score > 50:
lines.append(f"🔶 {name}: score {score}/100 — approaching threshold (65)")
if not signals and not any(
"cooldown" in r.get("reason","").lower() or
r.get("score",0) > 50
for _, r in waits
):
if in_session:
lines.append("📊 All strategies in WAIT — market likely in low-conviction state")
lines.append(" Hermes scanning every 5min cycle. Will fire when conditions align.")
elif not is_weekend:
lines.append("📊 Strategies dormant during off-hours — normal behaviour")
return lines
def build_report(acc, positions, scan_results, heph_result, journal_summaries) -> str:
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
balance = float(acc.get("balance",0))
equity = float(acc.get("equity",0))
profit = float(acc.get("profit",0))
lines = [
f"🧠 *GENESIS — Hourly Brain Feed*",
f"📅 {now}",
f"",
f"━━━━━ 💰 ACCOUNT ━━━━━",
f"Balance: `€{balance:,.2f}` | Equity: `€{equity:,.2f}`",
f"Floating P&L: `€{profit:+.2f}`",
f"",
]
# ── Open Positions ─────────────────────────────────────────────
lines.append("━━━━━ 📈 OPEN POSITIONS ━━━━━")
if isinstance(positions, list) and positions:
for p in positions:
pnl_val = float(p.get("profit",0))
pnl_icon = "🟢" if pnl_val >= 0 else "🔴"
lines.append(
f"{pnl_icon} `{p.get('symbol')}` {p.get('orderType')} "
f"{p.get('lots')}lot | P&L: `€{pnl_val:+.2f}` | [{p.get('comment')}]"
)
else:
lines.append(" No open positions")
# ── Journal summary (last hour) ────────────────────────────────
lines.append(f"")
lines.append("━━━━━ 📒 LAST HOUR TRADES ━━━━━")
total_trades = total_wins = total_losses = 0
total_pnl = 0.0
any_activity = False
for strat, summ in journal_summaries.items():
if summ["trades"] > 0:
any_activity = True
total_trades += summ["trades"]
total_wins += summ["wins"]
total_losses += summ["losses"]
total_pnl += summ["pnl"]
lines.append(
f" `{strat}`: {summ['trades']} trade(s) | "
f"{summ['wins']}W {summ['losses']}L | "
f"P&L: `€{summ['pnl']:+.2f}`"
)
if not any_activity:
lines.append(" No completed trades in the last hour")
else:
lines.append(f" *Total:* {total_trades} trades | "
f"{total_wins}W {total_losses}L | `€{total_pnl:+.2f}`")
# ── Strategy Scans ─────────────────────────────────────────────
lines.append(f"")
lines.append("━━━━━ 🔬 STRATEGY SCANS ━━━━━")
for name, result in scan_results.items():
lines.append(fmt_strategy_result(name, result))
lines.append(fmt_heph_status(heph_result))
# ── Next Hour Outlook ──────────────────────────────────────────
lines.append(f"")
lines.append("━━━━━ 🔭 NEXT HOUR OUTLOOK ━━━━━")
for l in estimate_next_hour(scan_results):
lines.append(l)
lines.append(f"")
lines.append("_Next broadcast in ~60 min_")
return "\n".join(lines)
def run_broadcast():
log.info("=== Brain Feed broadcast starting ===")
start = time.time()
# ── Parallel strategy scans ────────────────────────────────────
scan_tasks = {
"ARES": ("strategies/ares/ares_tool.py", "analyze", "EURUSDxx"),
"APOLLO": ("strategies/apollo/apollo_tool.py", "analyze", "EURUSDxx"),
"ATHENA": ("strategies/athena/athena_tool.py", "analyze", "EURUSDxx"),
"ARTEMIS": ("strategies/artemis/artemis_tool.py", "analyze", "EURUSDxx"),
"ZEUS": ("strategies/zeus/zeus_tool.py", "analyze", "EURUSDxx"),
}
scan_results = {}
heph_result = {}
with ThreadPoolExecutor(max_workers=6) as ex:
futures = {
ex.submit(run_tool, name, tool, cmd, sym): name
for name, (tool, cmd, sym) in scan_tasks.items()
}
futures[ex.submit(run_tool, "HEPH", "strategies/hephaestus/hephaestus_tool.py", "status", "")] = "HEPH"
for fut in as_completed(futures, timeout=SCAN_TIMEOUT+10):
name = futures[fut]
try:
result = fut.result(timeout=1)
if name == "HEPH":
heph_result = result
else:
scan_results[name] = result
except Exception as e:
if name == "HEPH": heph_result = {"error": str(e)[:60]}
else: scan_results[name] = {"action":"wait","reason":f"Scan error: {str(e)[:60]}"}
# ── Account + positions ────────────────────────────────────────
acc = bridge("/balance")
positions = bridge("/positions")
if not isinstance(positions, list): positions = []
# ── Journal summaries ──────────────────────────────────────────
journal_summaries = {tag: journal_summary(path, hours=1)
for tag, path in JOURNALS.items()}
# ── Build and send ─────────────────────────────────────────────
report = build_report(acc, positions, scan_results, heph_result, journal_summaries)
tg(report)
elapsed = round(time.time() - start, 1)
log.info(f"=== Brain Feed sent ({elapsed}s) ===")
if __name__ == "__main__":
run_broadcast()
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
GENESIS Daily P&L Report
Runs at 17:00 UTC. Sends a full daily performance summary to Telegram.
"""
import json, os, sys, requests
from datetime import datetime, timezone, timedelta
from pathlib import Path
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
CHAT = os.getenv("TELEGRAM_CHAT_ID", "")
sys.path.insert(0, str(Path(__file__).parent))
from mt5_bridge import bridge as _bridge
def api(path):
return _bridge(path)
def tg(msg):
if not TOKEN: return
try:
requests.post(f"https://api.telegram.org/bot{TOKEN}/sendMessage",
json={"chat_id": CHAT, "text": msg, "parse_mode": "Markdown"}, timeout=10)
except: pass
now = datetime.now(timezone.utc)
today = now.date()
acc = api("/balance")
balance = float(acc.get("balance", 0))
equity = float(acc.get("equity", 0))
profit = float(acc.get("profit", 0))
pos = api("/positions")
open_pos = pos if isinstance(pos, list) else []
hist = api("/history")
trades_today = []
if isinstance(hist, list):
for t in hist:
if t.get("entry") != 1:
continue
try:
deal_time = t.get("time", "")
if deal_time:
ct = datetime.fromisoformat(str(deal_time).replace("Z", "+00:00"))
if ct.date() == today:
trades_today.append(t)
except:
pass
wins = [t for t in trades_today if float(t.get("profit", 0)) > 0]
losses = [t for t in trades_today if float(t.get("profit", 0)) < 0]
be = [t for t in trades_today if float(t.get("profit", 0)) == 0]
total_pnl = sum(float(t.get("profit", 0)) for t in trades_today)
best = max(trades_today, key=lambda t: float(t.get("profit",0)), default=None)
worst = min(trades_today, key=lambda t: float(t.get("profit",0)), default=None)
win_rate = round(len(wins)/len(trades_today)*100) if trades_today else 0
by_strat = {}
for t in trades_today:
tag = t.get("comment", "UNKNOWN").split("-")[0]
by_strat.setdefault(tag, {"trades": 0, "pnl": 0.0})
by_strat[tag]["trades"] += 1
by_strat[tag]["pnl"] += float(t.get("profit", 0))
open_lines = []
for p in open_pos:
sym = p.get("symbol","?")
side = p.get("orderType","?").upper()
lots = p.get("lots","?")
pnl = float(p.get("profit", 0))
tag = p.get("comment","?")
icon = "📈" if side == "BUY" else "📉"
open_lines.append(f" {icon} {sym} {side} {lots}L [{tag}] €{pnl:+.2f}")
pnl_icon = "🟢" if total_pnl >= 0 else "🔴"
msg = (
f"📊 *GENESIS Daily Report — {today.strftime('%d %b %Y')}*\n"
f"{''*32}\n"
f"*Account*\n"
f" Balance: €{balance:,.2f}\n"
f" Equity: €{equity:,.2f}\n"
f" Float: €{profit:+.2f}\n\n"
f"*Today's Performance*\n"
f" {pnl_icon} P&L: €{total_pnl:+.2f}\n"
f" 📋 Trades: {len(trades_today)} ({len(wins)}W / {len(losses)}L / {len(be)}BE)\n"
f" 🎯 Win Rate: {win_rate}%\n"
)
if best:
msg += f" 🏆 Best: €{float(best.get('profit',0)):+.2f} ({best.get('symbol','')} [{best.get('comment','')}])\n"
if worst and worst != best:
msg += f" 💀 Worst: €{float(worst.get('profit',0)):+.2f} ({worst.get('symbol','')} [{worst.get('comment','')}])\n"
if by_strat:
msg += f"\n*By Strategy*\n"
for tag, data in sorted(by_strat.items()):
icon = "🟢" if data["pnl"] >= 0 else "🔴"
msg += f" {icon} {tag}: {data['trades']} trades | €{data['pnl']:+.2f}\n"
if open_pos:
msg += f"\n*Open Positions ({len(open_pos)})*\n"
msg += "\n".join(open_lines) + "\n"
else:
msg += f"\n*Open Positions:* None\n"
if not trades_today:
msg += "\n_No closed trades today._\n"
tg(msg)
print(msg)
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
GENESIS Market Open Intensive Scanner — runs every 30 seconds.
Only does heavy work during the first 30 minutes of London, NY, and Asia opens.
Exits silently the rest of the time (no AI cost, no noise).
"""
import json, subprocess, sys
from datetime import datetime, timezone
from pathlib import Path
PYTHON = "/opt/hermes-agent/.venv-hermes/bin/python3"
AGENT = "/opt/hermes-agent"
sys.path.insert(0, str(Path(__file__).parent))
from mt5_bridge import bridge as _bridge
now = datetime.now(timezone.utc)
hr = now.hour
mn = now.minute
wd = now.weekday()
# Market open windows (first 30 minutes of each session)
# London: 07:0007:30 UTC
# NY: 13:0013:30 UTC (13:00 = 9am NY time)
# Asia: 22:0022:30 UTC (22:00 = Tokyo midnight open)
OPEN_WINDOWS = [
{"name": "London Open", "h": 7, "pairs": ["EURUSDxx","GBPUSDxx","EURGBPxx"]},
{"name": "New York Open","h": 13, "pairs": ["EURUSDxx","GBPUSDxx","XAUUSDxx"]},
{"name": "Asia Open", "h": 22, "pairs": ["XAUUSDxx","GBPJPYxx","USDJPYxx"]},
]
# Only fire during an open window
active_window = None
for w in OPEN_WINDOWS:
if hr == w["h"] and mn < 30 and wd < 5:
active_window = w
break
if not active_window:
sys.exit(0) # Silent exit — not an open window
import requests
def api(path):
return _bridge(path)
def tool(name, cmd, sym):
args = [PYTHON, f"{AGENT}/{name}_tool.py", cmd, sym]
try:
r = subprocess.run(args, capture_output=True, text=True, timeout=25, cwd=AGENT)
return json.loads(r.stdout.strip()) if r.stdout.strip() else {}
except: return {}
acc = api("/balance")
balance = float(acc.get("balance", 0))
equity = float(acc.get("equity", 0))
print(f"=== {active_window['name'].upper()} INTENSIVE SCAN ===")
print(f"Time: {now.strftime('%H:%M')} UTC | Balance=€{balance:.2f} Equity=€{equity:.2f}")
print(f"Scanning: {', '.join(active_window['pairs'])}")
signals = []
for sym in active_window["pairs"]:
# Zeus is best at market opens (killzone)
r = tool("zeus", "analyze", sym)
if r.get("action") == "trade":
signals.append(("ZEUS", sym, r))
print(f" ⚡ ZEUS/{sym}: SIGNAL {r.get('direction')} | Score={r.get('confidence_score','?')} | RR={r.get('rr_ratio','?')}")
continue
# Apollo for trend-following at opens
r = tool("apollo", "analyze", sym)
if r.get("action") == "trade":
signals.append(("APOLLO", sym, r))
print(f" 🏹 APOLLO/{sym}: SIGNAL {r.get('direction')} | RR={r.get('rr_ratio','?')}")
continue
print(f"{sym}: no signal")
if signals:
print(f"\n{len(signals)} signal(s) at {active_window['name']}!")
for strat, sym, r in signals:
clean = sym.replace("xx","")
print(f" EXECUTE: {PYTHON} {AGENT}/{strat.lower()}_tool.py execute {clean}")
print(f"\nThis is a HIGH-PRIORITY window ({active_window['name']}).")
print("Execute the best signal immediately if conditions confirm.")
else:
print(f"\nNo signals at {active_window['name']} open yet. Continue monitoring.")
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""
GENESIS Trade Monitor — zero LLM cost.
Runs every minute. Sends Telegram ONLY when a trade opens or closes.
No AI, no summaries, no noise.
"""
import json, os, sys, requests
from datetime import datetime, timezone
from pathlib import Path
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
CHAT = os.getenv("TELEGRAM_CHAT_ID", "")
SNAP = Path("/var/log/hermes/position_snapshot.json")
sys.path.insert(0, str(Path(__file__).parent))
from mt5_bridge import bridge as _bridge
def tg(msg):
try:
requests.post(f"https://api.telegram.org/bot{TOKEN}/sendMessage",
json={"chat_id": CHAT, "text": msg}, timeout=10)
except: pass
def api(path):
return _bridge(path)
def pip_val(sym):
if "JPY" in sym: return 0.01
if "XAU" in sym or "GOLD" in sym: return 0.1
return 0.0001
acc = api("/balance")
pos = api("/positions")
balance = float(acc.get("balance", 0))
equity = float(acc.get("equity", 0))
open_pos = pos if isinstance(pos, list) else []
now = datetime.now(timezone.utc).strftime("%H:%M UTC")
prev = {}
if SNAP.exists():
try: prev = json.loads(SNAP.read_text())
except: prev = {}
curr = {str(p.get("ticket")): p for p in open_pos}
for ticket, p in curr.items():
if ticket not in prev:
sym = p.get("symbol","?")
side = p.get("orderType","?").upper()
lots = p.get("lots","?")
entry = p.get("openPrice","?")
sl = p.get("sl","?")
tp = p.get("tp","?")
strat = p.get("comment","?")
pip = pip_val(sym)
sl_pip = round(abs(float(entry)-float(sl))/pip, 1) if sl and entry and float(sl) != 0 else "?"
tp_pip = round(abs(float(tp)-float(entry))/pip, 1) if tp and entry and float(tp) != 0 else "?"
tg(
f"🟢 TRADE OPENED — {now}\n"
f"Strategy: {strat}\n"
f"{sym} {side} {lots} lot\n"
f"Entry: {entry}\n"
f"SL: {sl} (-{sl_pip} pips)\n"
f"TP: {tp} (+{tp_pip} pips)\n"
f"Balance: €{balance:,.2f}"
)
for ticket, p in prev.items():
if ticket not in curr:
sym = p.get("symbol","?")
side = p.get("orderType","?").upper()
lots = p.get("lots","?")
entry = p.get("openPrice","?")
strat = p.get("comment","?")
hist = api("/history")
pnl = None
if isinstance(hist, list):
for h in reversed(hist):
if h.get("entry") != 1:
continue
if h.get("symbol","").upper() == sym.upper():
h_comment = str(h.get("comment",""))
p_comment = str(strat)
if h_comment and p_comment and h_comment.split("-")[0] == p_comment.split("-")[0]:
pnl = float(h.get("profit", 0))
break
if pnl is None:
for h in reversed(hist):
if h.get("entry") != 1:
continue
if h.get("symbol","").upper() == sym.upper():
pnl = float(h.get("profit", 0))
break
icon = "🟢" if (pnl or 0) >= 0 else "🔴"
pnl_str = f"{pnl:+.2f}" if pnl is not None else "see MT5"
tg(
f"{icon} TRADE CLOSED — {now}\n"
f"Strategy: {strat}\n"
f"{sym} {side} {lots} lot\n"
f"Entry: {entry}\n"
f"Result: {pnl_str}\n"
f"Balance: €{balance:,.2f}"
)
SNAP.parent.mkdir(parents=True, exist_ok=True)
SNAP.write_text(json.dumps(curr))
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
GENESIS Heartbeat Monitor
Runs every 60 minutes. Sends a system health report to Telegram.
If this message stops appearing, the VPS is down.
"""
import os, requests, json, subprocess, sys
from datetime import datetime, timezone
from pathlib import Path
try:
from dotenv import load_dotenv
load_dotenv()
except:
pass
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
sys.path.insert(0, str(Path(__file__).parent))
from mt5_bridge import bridge as _bridge
default_journal = "/var/log/hermes/trade_journal.jsonl"
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
JOURNAL = Path(__file__).parents[1] / "logs" / "hermes" / "trade_journal.jsonl"
JOURNAL.parent.mkdir(parents=True, exist_ok=True)
default_ares_journal = "/var/log/ares/trade_journal.jsonl"
try:
Path(default_ares_journal).parent.mkdir(parents=True, exist_ok=True)
ARES_JOURNAL = Path(default_ares_journal)
except Exception:
ARES_JOURNAL = Path(__file__).parents[1] / "logs" / "ares" / "trade_journal.jsonl"
ARES_JOURNAL.parent.mkdir(parents=True, exist_ok=True)
def tg(msg):
try:
requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": msg, "parse_mode": "Markdown"}, timeout=10)
except: pass
def check_bridge_health():
try:
d = _bridge("/health")
if isinstance(d, dict):
s = d.get("status", "").lower()
return s in ("ok", "healthy", "running", "alive")
return True
except:
return False
def check_llm_health():
try:
import requests as _req
base = os.getenv("OPENAI_BASE_URL", "")
key = os.getenv("OPENAI_API_KEY", "")
if not base or not key:
return False
r = _req.get(base.replace("/v1", ""),
headers={"Authorization": f"Bearer {key}"},
timeout=10)
return r.status_code == 200
except:
return False
def main():
now = datetime.now(timezone.utc)
issues = []
balance_str = "N/A"
equity_str = "N/A"
pnl_str = "N/A"
try:
d = _bridge("/balance")
balance_str = f"{d.get('balance', 0):.2f}"
equity_str = f"{d.get('equity', 0):.2f}"
pnl_str = f"{d.get('profit', 0):.2f}"
except:
issues.append("🔴 Bridge DOWN")
positions_str = "None"
pos = []
try:
pos = _bridge("/positions")
if isinstance(pos, list) and len(pos) > 0:
p = pos[0]
positions_str = f"{p.get('symbol')} {p.get('orderType')} {p.get('lots')}lot | P&L: €{p.get('profit', 0):.2f}"
except:
issues.append("🔴 Cannot read positions")
if not check_bridge_health():
issues.append("🔴 MT5 bridge not reachable")
if not check_llm_health():
issues.append("🟡 LLM API not reachable")
ares_balance_str = balance_str
if equity_str != "N/A":
ares_balance_str = f"{balance_str} (eq {equity_str})"
ares_pos_str = "None"
if isinstance(pos, list):
ares_positions = [
f"{p.get('symbol')} {p.get('orderType')} {p.get('lots')}lot | P&L: €{p.get('profit', 0):.2f}"
for p in pos if "ARES" in str(p.get("comment", "")).upper()
]
if ares_positions:
ares_pos_str = ares_positions[0]
ares_wins = ares_losses = 0
if ARES_JOURNAL.exists():
for line in ARES_JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t = json.loads(line)
if t.get("result") == "win": ares_wins += 1
if t.get("result") == "loss": ares_losses += 1
except: pass
wins, losses = 0, 0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t = json.loads(line)
if t.get("result") == "win": wins += 1
if t.get("result") == "loss": losses += 1
except: pass
status = "✅ ALL SYSTEMS NOMINAL" if not issues else "\n".join(issues)
msg = (
f"🤖 *GENESIS HEARTBEAT*\n"
f"🕐 {now.strftime('%Y-%m-%d %H:%M')} UTC\n\n"
f"*System Status:* {status}\n\n"
f"━━━ ⚡ HERMES (Account A) ━━━\n"
f"💰 Balance: {balance_str}\n"
f"📊 Equity: {equity_str}\n"
f"📈 Open P&L: {pnl_str}\n"
f"🔓 Position: {positions_str}\n"
f"📒 History: {wins}W / {losses}L\n\n"
f"━━━ ⚔️ ARES (Account B) ━━━\n"
f"💰 Balance: {ares_balance_str}\n"
f"🔓 Position: {ares_pos_str}\n"
f"📒 History: {ares_wins}W / {ares_losses}L\n\n"
f"_Next heartbeat in 60 minutes._"
)
tg(msg)
if __name__ == "__main__":
main()
+357
View File
@@ -0,0 +1,357 @@
#!/usr/bin/env python3
"""
GENESIS — Mt5Bridge Unified Adapter
Provides a bridge() function with the SAME call interface as the old API2TRADE version,
but internally routes all calls to the Mt5Bridge REST API.
Usage:
from core.mt5_bridge import bridge, get_bars, pip_size
acc = bridge("/balance")
pos = bridge("/positions")
q = bridge("/quote?symbol=EURUSD")
ord = bridge("/market", "POST", {"symbol":"EURUSD","type":"Buy","volume":0.1,
"stop_loss":1.08,"take_profit":1.09,"comment":"TEST"})
bridge("/close", "POST", {"ticket": 12345})
bridge("/modify", "POST", {"ticket": 12345, "stop_loss": 1.07})
Environment variables:
MT5_BRIDGE_URL — Base URL (default: http://61.164.252.86:13485)
MT5_BRIDGE_KEY — API Key for X-API-Key header
MT5_SYMBOL_MAP — JSON string mapping xx-suffix symbols to broker symbols
e.g. '{"EURUSDxx":"EURUSD","XAUUSDxx":"XAUUSDc"}'
Mt5Bridge API docs: see Mt5Bridge使用指南.md
"""
import os, json, logging, math
from datetime import datetime, timezone, timedelta
from pathlib import Path
import requests
log = logging.getLogger(__name__)
BRIDGE_URL = os.getenv("MT5_BRIDGE_URL", "http://61.164.252.86:13485")
BRIDGE_KEY = os.getenv("MT5_BRIDGE_KEY", "")
_SYMBOL_MAP_RAW = os.getenv("MT5_SYMBOL_MAP", "")
if _SYMBOL_MAP_RAW:
try:
SYMBOL_MAP = json.loads(_SYMBOL_MAP_RAW)
except json.JSONDecodeError:
SYMBOL_MAP = {}
else:
SYMBOL_MAP = {}
_HEADERS = {"X-API-Key": BRIDGE_KEY, "Content-Type": "application/json"}
def resolve_symbol(sym: str) -> str:
if sym in SYMBOL_MAP:
resolved = SYMBOL_MAP[sym]
if resolved != sym:
log.debug(f"resolve_symbol: {sym}{resolved} (MAP)")
return resolved
if sym.endswith("xx"):
base = sym[:-2]
if base in SYMBOL_MAP:
resolved = SYMBOL_MAP[base]
log.debug(f"resolve_symbol: {sym}{resolved} (MAP via base)")
return resolved
log.debug(f"resolve_symbol: {sym}{base} (strip xx)")
return base
return sym
def _api_get(path: str, params=None) -> dict:
try:
r = requests.get(f"{BRIDGE_URL}{path}", params=params,
headers=_HEADERS, timeout=15)
r.raise_for_status()
return r.json()
except Exception as e:
log.error(f"Mt5Bridge GET {path}: {e}")
return {}
def _api_post(path: str, data: dict) -> dict:
try:
r = requests.post(f"{BRIDGE_URL}{path}", json=data,
headers=_HEADERS, timeout=15)
r.raise_for_status()
return r.json()
except Exception as e:
log.error(f"Mt5Bridge POST {path}: {e}")
return {}
def bridge(path, method="GET", data=None) -> dict:
"""
Unified bridge interface — same signature as the old API2TRADE version.
Supported paths:
/balance → GET /account
/positions → GET /positions
/history → GET /history/deals (today)
/quote?symbol=X → GET /symbols/{sym}/tick
/market (POST) → POST /order/send
/close (POST) → POST /position/close
/modify (POST) → POST /position/modify
/symbols/{sym} → GET /symbols/{sym}
/rates?symbol=X&tf=M5&count=100 → GET /rates/from-pos
"""
# ── Quote ──────────────────────────────────────────────────────────────
if path.startswith("/quote"):
sym = path.split("symbol=")[-1] if "symbol=" in path else ""
if not sym and data:
sym = data.get("symbol", "")
sym = resolve_symbol(sym)
raw = _api_get(f"/symbols/{sym}/tick")
items = raw.get("data", [])
if items:
t = items[0]
return {
"bid": float(t.get("bid", 0)),
"ask": float(t.get("ask", 0)),
"symbol": sym,
}
return {"bid": 0, "ask": 0, "symbol": sym}
# ── Balance / Account ──────────────────────────────────────────────────
if path == "/balance":
raw = _api_get("/account")
items = raw.get("data", [])
if not items:
raw = _api_get("/account")
items = raw.get("data", [])
if items:
a = items[0]
return {
"balance": float(a.get("balance", 0)),
"equity": float(a.get("equity", 0)),
"margin": float(a.get("margin", 0)),
"profit": float(a.get("profit", 0)),
"margin_free": float(a.get("margin_free", 0)),
"margin_level": float(a.get("margin_level", 0)),
"leverage": int(a.get("leverage", 0)),
"currency": a.get("currency", "USD"),
}
return {"balance": 0, "equity": 0, "margin": 0, "profit": 0}
# ── Positions ──────────────────────────────────────────────────────────
if path == "/positions":
sym_filter = None
if data and data.get("symbol"):
sym_filter = resolve_symbol(data["symbol"])
raw = _api_get("/positions", params={"symbol": sym_filter} if sym_filter else None)
items = raw.get("data", [])
return [{
"ticket": p.get("ticket", 0),
"symbol": p.get("symbol", ""),
"orderType": "BUY" if p.get("type", 0) == 0 else "SELL",
"type": p.get("type", 0),
"lots": float(p.get("volume", 0)),
"volume": float(p.get("volume", 0)),
"openPrice": float(p.get("price_open", 0)),
"price_open": float(p.get("price_open", 0)),
"price_current": float(p.get("price_current", 0)),
"sl": float(p.get("sl", 0)),
"tp": float(p.get("tp", 0)),
"profit": float(p.get("profit", 0)),
"swap": float(p.get("swap", 0)),
"comment": p.get("comment", ""),
"magic": p.get("magic", 0),
} for p in items]
# ── History ────────────────────────────────────────────────────────────
if path == "/history":
now = datetime.now(timezone.utc)
date_from = now.strftime("%Y-%m-%d")
date_to = (now + timedelta(days=1)).strftime("%Y-%m-%d")
raw = _api_get("/history/deals", params={
"date_from": date_from,
"date_to": date_to,
})
items = raw.get("data", [])
return [{
"ticket": d.get("ticket", 0),
"symbol": d.get("symbol", ""),
"type": d.get("type", 0),
"entry": d.get("entry", 0),
"volume": float(d.get("volume", 0)),
"price": float(d.get("price", 0)),
"profit": float(d.get("profit", 0)),
"commission": float(d.get("commission", 0)),
"swap": float(d.get("swap", 0)),
"comment": d.get("comment", ""),
"magic": d.get("magic", 0),
"time": d.get("time", ""),
} for d in items]
# ── Place order ────────────────────────────────────────────────────────
if path == "/market" and data:
sym = resolve_symbol(data.get("symbol", ""))
direction = data.get("type", "Buy")
order_type = 0 if direction.lower() in ("buy", "long") else 1
tick_data = _api_get(f"/symbols/{sym}/tick")
tick_items = tick_data.get("data", [])
price = 0
if tick_items:
price = float(tick_items[0].get("ask" if order_type == 0 else "bid", 0))
request_obj = {
"action": 1,
"symbol": sym,
"volume": float(data.get("volume", 0.01)),
"order_type": order_type,
"price": price,
"sl": 0,
"tp": 0,
"magic": int(data.get("magic", 88001)),
"comment": data.get("comment", "GENESIS"),
"deviation": 10,
"type_filling": 0,
}
sl_val = data.get("stop_loss")
tp_val = data.get("take_profit")
if sl_val is not None and float(sl_val) != 0:
request_obj["sl"] = float(sl_val)
if tp_val is not None and float(tp_val) != 0:
request_obj["tp"] = float(tp_val)
payload = {"request": request_obj}
raw = _api_post("/order/send", payload)
resp_data = raw.get("data", raw)
ticket = resp_data.get("order") or resp_data.get("ticket")
retcode = resp_data.get("retcode", 0)
if retcode == 10009 and ticket:
return {"ticket": ticket}
return {"ticket": ticket, "retcode": retcode,
"comment": resp_data.get("comment", "")}
# ── Close position ─────────────────────────────────────────────────────
if path == "/close" and data:
ticket = data.get("ticket")
payload = {"ticket": int(ticket)}
if data.get("volume"):
payload["volume"] = float(data["volume"])
raw = _api_post("/position/close", payload)
resp_data = raw.get("data", raw)
retcode = resp_data.get("retcode", 0)
if retcode == 10009:
return {"message": "ok"}
return {"retcode": retcode, "comment": resp_data.get("comment", "")}
# ── Modify position ────────────────────────────────────────────────────
if path == "/modify" and data:
ticket = data.get("ticket")
payload = {"ticket": int(ticket)}
if data.get("stop_loss") is not None:
payload["sl"] = float(data["stop_loss"])
if data.get("take_profit") is not None:
payload["tp"] = float(data["take_profit"])
raw = _api_post("/position/modify", payload)
resp_data = raw.get("data", raw)
retcode = resp_data.get("retcode", 0)
if retcode == 10009:
return {"ok": True}
return {"retcode": retcode, "comment": resp_data.get("comment", "")}
# ── Symbol info ────────────────────────────────────────────────────────
if path.startswith("/symbols/"):
sym = path.split("/symbols/")[-1].split("?")[0]
sym = resolve_symbol(sym)
raw = _api_get(f"/symbols/{sym}")
items = raw.get("data", [])
return items[0] if items else {}
# ── K-line rates ───────────────────────────────────────────────────────
if path == "/rates" and data:
sym = resolve_symbol(data.get("symbol", ""))
tf = data.get("timeframe", "M5")
count = data.get("count", 100)
raw = _api_get("/rates/from-pos", params={
"symbol": sym,
"timeframe": f"TIMEFRAME_{tf}",
"start_pos": 0,
"count": count,
})
return raw.get("data", [])
# ── Health check ───────────────────────────────────────────────────────
if path == "/health":
return _api_get("/health")
# ── Fallback: pass through to bridge ───────────────────────────────────
if method == "POST" and data:
return _api_post(path, data)
return _api_get(path, params=data if isinstance(data, dict) else None)
def pip_size(symbol: str) -> float:
s = symbol.upper()
if "JPY" in s:
return 0.01
if "XAU" in s or "GOLD" in s:
return 0.1
return 0.0001
def get_bars(symbol: str, tf: str = "M5", count: int = 100) -> list:
"""
Fetch OHLCV bars from Mt5Bridge /rates/from-pos.
tf: M1, M5, M15, M30, H1, H4, D1
Returns list of dicts with: time, open, high, low, close, tick_volume
"""
sym = resolve_symbol(symbol)
raw = _api_get("/rates/from-pos", params={
"symbol": sym,
"timeframe": f"TIMEFRAME_{tf}",
"start_pos": 0,
"count": count,
})
items = raw.get("data", [])
if not items:
return []
import pandas as pd
df = pd.DataFrame(items)
if "time" in df.columns:
df["time"] = pd.to_datetime(df["time"])
df.columns = [c.lower().replace("tick_volume", "volume") for c in df.columns]
return df.to_dict("records")
def get_bars_by_date(symbol: str, tf: str = "H1",
date_from: str = "", date_to: str = "") -> list:
sym = resolve_symbol(symbol)
params = {
"symbol": sym,
"timeframe": f"TIMEFRAME_{tf}",
}
if date_from:
params["date_from"] = date_from
if date_to:
params["date_to"] = date_to
raw = _api_get("/rates/from-date", params=params)
items = raw.get("data", [])
if not items:
return []
import pandas as pd
df = pd.DataFrame(items)
if "time" in df.columns:
df["time"] = pd.to_datetime(df["time"])
df.columns = [c.lower().replace("tick_volume", "volume") for c in df.columns]
return df.to_dict("records")
def calc_lot(equity: float, sl_pips: float, symbol: str, risk_pct: float = 0.01) -> float:
pip_val_per_lot = 10.0
s = symbol.upper()
if "JPY" in s:
pip_val_per_lot = 9.0
if "GBP" in s:
pip_val_per_lot = 12.5
if "XAU" in s or "GOLD" in s:
pip_val_per_lot = 1.0
raw_lot = (equity * risk_pct) / (sl_pips * pip_val_per_lot) if sl_pips > 0 else 0.01
raw_lot = max(0.01, min(raw_lot, 5.0))
return round(round(raw_lot / 0.01) * 0.01, 2)
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""Quick Telegram notification helper used by cron scripts."""
import os, requests
from dotenv import load_dotenv
from pathlib import Path
load_dotenv(Path(__file__).parent.parent / ".env")
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
CHAT = os.getenv("TELEGRAM_CHAT_ID", "")
def notify(msg: str) -> bool:
if not TOKEN or not CHAT:
return False
try:
r = requests.post(
f"https://api.telegram.org/bot{TOKEN}/sendMessage",
json={"chat_id": CHAT, "text": msg, "parse_mode": "Markdown"},
timeout=10,
)
return r.status_code == 200
except Exception:
return False
if __name__ == "__main__":
import sys
notify(sys.argv[1] if len(sys.argv) > 1 else "GENESIS test notification")
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
services:
genesis:
build:
context: .
dockerfile: Dockerfile
container_name: genesis
restart: unless-stopped
# ── Mount source code + config ──────────────────────────────────────────
# Code changes only need: docker compose restart
# Dependency changes need: docker compose up -d --build
volumes:
- ./core:/opt/hermes-agent/core
- ./strategies:/opt/hermes-agent/strategies
- ./configs:/opt/hermes-agent/configs
- ./backtest:/opt/hermes-agent/backtest
- ./.env:/opt/hermes-agent/.env:ro
- genesis_logs:/var/log/hermes
- genesis_strategy_logs:/var/log
- genesis_cache:/tmp
# ── Environment overrides (optional) ────────────────────────────────────
environment:
- TZ=UTC
- GENESIS_MODE=live # Set to "analyze" to disable execution
# ── Health check — confirms Mt5Bridge is reachable ──────────────────────
healthcheck:
test: ["CMD", "python3", "-c",
"import os,requests; r=requests.get(os.getenv('MT5_BRIDGE_URL','http://localhost:13485')+'/health',headers={'X-API-Key':os.getenv('MT5_BRIDGE_KEY','')},timeout=5); exit(0 if r.status_code==200 else 1)"]
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
# ── Resource limits ──────────────────────────────────────────────────────
mem_limit: 512m
cpus: "0.5"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
genesis_logs:
genesis_strategy_logs:
genesis_cache:
+194
View File
@@ -0,0 +1,194 @@
#!/bin/bash
# ═══════════════════════════════════════════════════════════════════════════════
# GENESIS Docker Entrypoint
# Validates credentials → sets up cron → starts autonomous engine
# ═══════════════════════════════════════════════════════════════════════════════
set -e
PYTHON="/opt/hermes-agent/.venv-hermes/bin/python3"
AGENT="/opt/hermes-agent"
echo ""
echo " ██████ ███████ ███ ██ ███████ ███████ ██ ███████"
echo " ██ ██ ████ ██ ██ ██ ██ ██"
echo " ██ ███ █████ ██ ██ ██ █████ ███████ ██ ███████"
echo " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██"
echo " ██████ ███████ ██ ████ ███████ ███████ ██ ███████"
echo ""
echo " Autonomous MT5 Trading System — Docker Mode"
echo " Powered by Mt5Bridge"
echo "─────────────────────────────────────────────────────"
# ── Load .env ─────────────────────────────────────────────────────────────────
if [ -f /opt/hermes-agent/.env ]; then
cp /opt/hermes-agent/.env /tmp/.env.fixed
sed -i 's/\r$//' /tmp/.env.fixed
set -a
source /tmp/.env.fixed
set +a
else
echo ""
echo " ╔═══════════════════════════════════════════════════╗"
echo " ║ NO .env FILE FOUND ║"
echo " ║ ║"
echo " ║ Mount your credentials file: ║"
echo " ║ docker run -v \$(pwd)/.env:/opt/hermes-agent/.env ║"
echo " ║ ║"
echo " ║ Or generate it first: ║"
echo " ║ bash setup.sh ║"
echo " ╚═══════════════════════════════════════════════════╝"
echo ""
exit 1
fi
# ── Validate required credentials ─────────────────────────────────────────────
echo ""
MISSING=0
PLACEHOLDER_PATTERN="YOUR_|CHANGE_ME|example|placeholder|sk-your"
check_var() {
local var_name="$1"
local var_val="${!var_name}"
if [ -z "$var_val" ] || echo "$var_val" | grep -qiE "$PLACEHOLDER_PATTERN"; then
echo "$var_name — not configured"
MISSING=$((MISSING + 1))
else
local masked="${var_val:0:8}••••"
echo "$var_name = $masked"
fi
}
echo " Credential check:"
check_var MT5_BRIDGE_URL
check_var MT5_BRIDGE_KEY
check_var OPENAI_API_KEY
check_var TELEGRAM_BOT_TOKEN
check_var TELEGRAM_CHAT_ID
if [ "$MISSING" -gt 0 ]; then
echo ""
echo " ╔═══════════════════════════════════════════════════════╗"
echo "$MISSING required credential(s) missing. ║"
echo " ║ ║"
echo " ║ Edit your .env file and restart: ║"
echo " ║ docker compose restart ║"
echo " ╚═══════════════════════════════════════════════════════╝"
echo ""
exit 1
fi
# ── Verify Mt5Bridge connectivity ─────────────────────────────────────────────
echo ""
echo " Verifying Mt5Bridge connection..."
BRIDGE_URL="${MT5_BRIDGE_URL:-http://localhost:13485}"
BRIDGE_KEY="${MT5_BRIDGE_KEY:-}"
ACCT_JSON=$(curl -s \
-H "X-API-Key: $BRIDGE_KEY" \
"$BRIDGE_URL/account" \
--max-time 10 2>/dev/null || echo '{}')
BALANCE=$(echo "$ACCT_JSON" | $PYTHON -c "
import sys, json
try:
d = json.load(sys.stdin)
items = d.get('data', [])
if items and len(items) > 0:
a = items[0]
b = a.get('balance', 0)
c = a.get('currency', 'USD')
print(f' ✓ Connected | Balance: {b:,.2f} {c}')
elif d.get('balance') is not None:
b = d.get('balance', 0)
c = d.get('currency', 'USD')
print(f' ✓ Connected | Balance: {b:,.2f} {c}')
else:
err = d.get('message', d.get('error', 'no data'))
if err:
print(f' ✗ API error: {err}')
else:
print(' ✗ No account data returned')
except Exception as e:
print(' ✗ Could not parse API response')
" 2>/dev/null || echo " ✗ Connection failed")
echo "$BALANCE"
if echo "$BALANCE" | grep -q "✗"; then
echo ""
echo " WARNING: Mt5Bridge connection failed."
echo " Check MT5_BRIDGE_URL and MT5_BRIDGE_KEY in .env"
echo " Starting anyway — strategies will retry each cycle."
echo ""
fi
# ── Export env for cron ────────────────────────────────────────────────────────
printenv | grep -E "^(MT5_|TELEGRAM_|OPENAI_|HERMES_|TWELVE_|FRED_|MAX_|SYMBOL_|HTTPS_PROXY)" > /etc/environment
# ── Install cron jobs ──────────────────────────────────────────────────────────
echo ""
echo " Installing cron jobs..."
cat > /etc/cron.d/genesis << CRONEOF
SHELL=/bin/bash
PATH=/opt/hermes-agent/.venv-hermes/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
BASH_ENV=/etc/environment
# GENESIS autonomous strategy scan — every 5 minutes
*/5 * * * * root cd $AGENT && $PYTHON $AGENT/core/genesis_autonomous.py >> /var/log/hermes/autonomous.log 2>&1
# Hermes LLM macro cycle — every hour
0 * * * * root cd $AGENT && $PYTHON $AGENT/core/trading_cycle.py >> /var/log/hermes/trading_cycle.log 2>&1
# Brain feed Telegram report — every hour at :30
30 * * * * root cd $AGENT && $PYTHON $AGENT/core/genesis_brain_feed.py >> /var/log/hermes/autonomous.log 2>&1
# Market open alert — weekdays 07:00 UTC
0 7 * * 1-5 root cd $AGENT && $PYTHON $AGENT/core/genesis_market_open.py >> /var/log/hermes/autonomous.log 2>&1
# Daily P&L report — 06:00 UTC
0 6 * * * root cd $AGENT && $PYTHON $AGENT/core/genesis_daily_report.py >> /var/log/hermes/autonomous.log 2>&1
# Heartbeat — every 10 minutes
*/10 * * * * root cd $AGENT && $PYTHON $AGENT/core/heartbeat.py >> /var/log/hermes/autonomous.log 2>&1
CRONEOF
chmod 644 /etc/cron.d/genesis
echo " ✓ 6 cron jobs installed"
# ── Start Telegram Bots ──────────────────────────────────────────────────────
echo ""
echo " Starting Telegram bots..."
for bot in ares apollo athena zeus; do
BOT_SCRIPT="$AGENT/strategies/${bot}/${bot}_telegram_bot.py"
if [ -f "$BOT_SCRIPT" ]; then
nohup $PYTHON "$BOT_SCRIPT" >> /var/log/hermes/${bot}_bot.log 2>&1 &
elif [ "$bot" = "zeus" ] && [ -f "$AGENT/strategies/zeus/zeus_tool.py" ]; then
nohup $PYTHON "$AGENT/strategies/zeus/zeus_tool.py" bot >> /var/log/hermes/zeus_bot.log 2>&1 &
else
echo "${bot}_telegram_bot not found"
continue
fi
echo "${bot}_telegram_bot started (PID $!)"
done
# ── Ready ─────────────────────────────────────────────────────────────────────
echo ""
echo "─────────────────────────────────────────────────────"
echo " ✓ GENESIS is running"
echo ""
echo " Telegram commands:"
echo " /ares_help /apollo_help /athena_help"
echo ""
echo " CLI commands:"
echo " docker exec -it genesis ares analyze EURUSD"
echo " docker exec -it genesis zeus analyze GBPUSD"
echo " docker exec -it genesis genesis-scan EURUSD"
echo " docker exec -it genesis tail -f /var/log/hermes/autonomous.log"
echo "─────────────────────────────────────────────────────"
echo ""
# ── Start cron + tail logs ────────────────────────────────────────────────────
service cron start
touch /var/log/hermes/autonomous.log /var/log/hermes/trading_cycle.log
tail -f /var/log/hermes/autonomous.log /var/log/hermes/trading_cycle.log
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# GENESIS Auto-Installer
# Tested on: Ubuntu 22.04 LTS
# Usage: bash install.sh
# ─────────────────────────────────────────────────────────────────────────────
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
INSTALL_DIR="/opt/hermes-agent"
VENV="$INSTALL_DIR/.venv-hermes"
PYTHON="$VENV/bin/python3"
PIP="$VENV/bin/pip"
echo ""
echo -e "${CYAN}${BOLD}"
echo " ██████ ███████ ███ ██ ███████ ███████ ██ ███████ "
echo " ██ ██ ████ ██ ██ ██ ██ ██ "
echo " ██ ███ █████ ██ ██ ██ █████ ███████ ██ ███████ "
echo " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ "
echo " ██████ ███████ ██ ████ ███████ ███████ ██ ███████ "
echo ""
echo -e "${NC}${BOLD} Autonomous Forex Trading System — Auto Installer${NC}"
echo -e " Powered by ${CYAN}API2TRADE${NC} (https://app.api2trade.com)"
echo ""
echo "─────────────────────────────────────────────────────"
# ── Step 1: System packages ───────────────────────────────────────────────────
echo -e "\n${YELLOW}[1/8] Installing system packages...${NC}"
sudo apt-get update -qq
sudo apt-get install -y -qq python3.11 python3.11-venv python3-pip git curl wget unzip
# ── Step 2: Create install directory ─────────────────────────────────────────
echo -e "\n${YELLOW}[2/8] Creating install directory at $INSTALL_DIR...${NC}"
sudo mkdir -p "$INSTALL_DIR"
sudo chown "$USER":"$USER" "$INSTALL_DIR"
# Copy all repo files into install dir
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cp -r "$SCRIPT_DIR"/. "$INSTALL_DIR/"
echo -e " ${GREEN}${NC} Files copied to $INSTALL_DIR"
# ── Step 3: Python virtual environment ───────────────────────────────────────
echo -e "\n${YELLOW}[3/8] Creating Python virtual environment...${NC}"
python3.11 -m venv "$VENV"
"$PIP" install --upgrade pip -q
"$PIP" install -r "$INSTALL_DIR/requirements.txt" -q
echo -e " ${GREEN}${NC} Virtual environment ready at $VENV"
# ── Step 4: Log directories ───────────────────────────────────────────────────
echo -e "\n${YELLOW}[4/8] Creating log directories...${NC}"
for strat in hermes ares apollo athena artemis zeus hephaestus; do
sudo mkdir -p "/var/log/$strat"
sudo chown "$USER":"$USER" "/var/log/$strat"
echo -e " ${GREEN}${NC} /var/log/$strat"
done
# ── Step 5: Environment file ──────────────────────────────────────────────────
echo -e "\n${YELLOW}[5/8] Setting up environment variables...${NC}"
ENV_FILE="$INSTALL_DIR/.env"
if [ -f "$ENV_FILE" ]; then
echo -e " ${CYAN}${NC} .env already exists — skipping (edit manually if needed)"
else
cp "$INSTALL_DIR/.env.example" "$ENV_FILE"
echo ""
echo -e " ${BOLD}You need to fill in your credentials.${NC}"
echo -e " Get your API2TRADE UUID + API Key at: ${CYAN}https://app.api2trade.com${NC}"
echo ""
read -p " API2TRADE Account UUID: " uuid
read -p " API2TRADE API Key: " apikey
read -p " Telegram Bot Token: " tgtoken
read -p " Telegram Chat ID: " tgchat
read -p " OpenRouter API Key: " openkey
sed -i "s/YOUR_API2TRADE_ACCOUNT_UUID/$uuid/g" "$ENV_FILE"
sed -i "s/YOUR_API2TRADE_API_KEY/$apikey/g" "$ENV_FILE"
sed -i "s/YOUR_TELEGRAM_BOT_TOKEN/$tgtoken/g" "$ENV_FILE"
sed -i "s/YOUR_TELEGRAM_CHAT_ID/$tgchat/g" "$ENV_FILE"
sed -i "s/YOUR_OPENAI_OR_OPENROUTER_KEY/$openkey/g" "$ENV_FILE"
chmod 600 "$ENV_FILE"
echo -e " ${GREEN}${NC} .env created and secured (chmod 600)"
fi
# ── Step 6: Config files — inject env values ──────────────────────────────────
echo -e "\n${YELLOW}[6/8] Linking config files...${NC}"
ln -sf "$INSTALL_DIR/configs" "$INSTALL_DIR/configs_active" 2>/dev/null || true
echo -e " ${GREEN}${NC} Configs ready at $INSTALL_DIR/configs/"
echo -e " ${CYAN}${NC} Edit configs/<strategy>_config.yaml to tune each strategy"
# ── Step 7: CLI shortcuts ─────────────────────────────────────────────────────
echo -e "\n${YELLOW}[7/8] Installing CLI shortcuts...${NC}"
install_shortcut() {
local name=$1
local tool=$2
cat > "/usr/local/bin/$name" << EOF
#!/usr/bin/env bash
$PYTHON $INSTALL_DIR/$tool "\$@"
EOF
sudo chmod +x "/usr/local/bin/$name"
}
sudo bash -c "cat > /usr/local/bin/ares << 'EOF'
#!/usr/bin/env bash
$PYTHON $INSTALL_DIR/strategies/ares/ares_tool.py \"\$@\"
EOF"
sudo bash -c "cat > /usr/local/bin/apollo << 'EOF'
#!/usr/bin/env bash
$PYTHON $INSTALL_DIR/strategies/apollo/apollo_tool.py \"\$@\"
EOF"
sudo bash -c "cat > /usr/local/bin/athena << 'EOF'
#!/usr/bin/env bash
$PYTHON $INSTALL_DIR/strategies/athena/athena_tool.py \"\$@\"
EOF"
sudo bash -c "cat > /usr/local/bin/artemis << 'EOF'
#!/usr/bin/env bash
$PYTHON $INSTALL_DIR/strategies/artemis/artemis_tool.py \"\$@\"
EOF"
sudo bash -c "cat > /usr/local/bin/zeus << 'EOF'
#!/usr/bin/env bash
$PYTHON $INSTALL_DIR/strategies/zeus/zeus_tool.py \"\$@\"
EOF"
sudo bash -c "cat > /usr/local/bin/hephaestus << 'EOF'
#!/usr/bin/env bash
$PYTHON $INSTALL_DIR/strategies/hephaestus/hephaestus_tool.py \"\$@\"
EOF"
sudo bash -c "cat > /usr/local/bin/genesis-scan << 'EOF'
#!/usr/bin/env bash
SYMBOL=\${1:-EURUSDxx}
echo \"\"
echo \"=== GENESIS FULL SCAN: \$SYMBOL ===\"
echo \"\"
for bot in ares apollo athena artemis zeus; do
echo \"--- \$bot ---\"
\$bot analyze \$SYMBOL 2>/dev/null | python3 -c \"import sys,json; d=json.load(sys.stdin); print(f' Action: {d.get(\\\"action\\\",\\\"?\\\")}' + (f' | {d.get(\\\"direction\\\",\\\"?\\\")} | R:R {d.get(\\\"rr_ratio\\\",\\\"?\\\")}' if d.get(\\\"action\\\")==\\\"trade\\\" else f' — {str(d.get(\\\"reason\\\",\\\"\\\"))[:80]}'))\" 2>/dev/null || echo \" (scan error)\"
done
echo \"\"
EOF"
sudo chmod +x /usr/local/bin/ares /usr/local/bin/apollo /usr/local/bin/athena \
/usr/local/bin/artemis /usr/local/bin/zeus /usr/local/bin/hephaestus \
/usr/local/bin/genesis-scan
echo -e " ${GREEN}${NC} CLI shortcuts: ares, apollo, athena, artemis, zeus, hephaestus, genesis-scan"
# ── Step 8: Cron jobs ─────────────────────────────────────────────────────────
echo -e "\n${YELLOW}[8/8] Installing cron jobs...${NC}"
CRON_TMP=$(mktemp)
crontab -l 2>/dev/null > "$CRON_TMP" || true
# Only add if not already present
add_cron() {
local entry=$1
grep -qF "$entry" "$CRON_TMP" || echo "$entry" >> "$CRON_TMP"
}
add_cron "*/5 * * * * cd $INSTALL_DIR && $PYTHON $INSTALL_DIR/core/genesis_autonomous.py >> /var/log/hermes/autonomous.log 2>&1"
add_cron "0 * * * * cd $INSTALL_DIR && $PYTHON $INSTALL_DIR/core/trading_cycle.py >> /var/log/hermes/trading_cycle.log 2>&1"
add_cron "30 * * * * cd $INSTALL_DIR && $PYTHON $INSTALL_DIR/core/genesis_brain_feed.py >> /var/log/hermes/autonomous.log 2>&1"
add_cron "0 7 * * 1-5 cd $INSTALL_DIR && $PYTHON $INSTALL_DIR/core/genesis_market_open.py >> /var/log/hermes/autonomous.log 2>&1"
add_cron "0 6 * * * cd $INSTALL_DIR && $PYTHON $INSTALL_DIR/core/genesis_daily_report.py >> /var/log/hermes/autonomous.log 2>&1"
add_cron "*/10 * * * * cd $INSTALL_DIR && $PYTHON $INSTALL_DIR/core/heartbeat.py >> /var/log/hermes/autonomous.log 2>&1"
crontab "$CRON_TMP"
rm "$CRON_TMP"
echo -e " ${GREEN}${NC} Cron jobs installed"
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
echo "─────────────────────────────────────────────────────"
echo -e "${GREEN}${BOLD} ✓ GENESIS installed successfully!${NC}"
echo "─────────────────────────────────────────────────────"
echo ""
echo -e " ${BOLD}Quick test:${NC}"
echo -e " ares analyze EURUSDxx"
echo -e " zeus analyze GBPUSDxx"
echo -e " genesis-scan EURUSDxx"
echo ""
echo -e " ${BOLD}Logs:${NC}"
echo -e " tail -f /var/log/hermes/autonomous.log"
echo -e " tail -f /var/log/hermes/trading_cycle.log"
echo ""
echo -e " ${BOLD}Docs:${NC}"
echo -e " ${CYAN}https://app.api2trade.com${NC}"
echo ""
echo -e " ${YELLOW}⚠ Hephaestus (Grid/Martingale) is DISABLED by default.${NC}"
echo -e " Read configs/hephaestus_config.yaml risk warning before enabling."
echo ""
+23
View File
@@ -0,0 +1,23 @@
# GENESIS — Python Dependencies
# Install with: pip install -r requirements.txt
# HTTP & API
requests==2.31.0
python-dotenv==1.0.0
# Config
pyyaml==6.0.1
# Market data
yfinance==0.2.36
pandas==2.1.4
# Technical indicators
ta==0.11.0
numpy==1.26.3
# Telegram bot
python-telegram-bot==20.7
# Scheduling (used by some telegram bots)
APScheduler==3.10.4
+23
View File
@@ -0,0 +1,23 @@
[Unit]
Description=GENESIS Ares Strategy B — Telegram Command Bot
Documentation=https://github.com/your-repo/genesis
After=network-online.target hermes-gateway.service
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/GENESIS
EnvironmentFile=/root/GENESIS/hermes_openrouter_env
ExecStart=/usr/bin/python3 /root/GENESIS/ares_telegram_bot.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
# Ares bot logs
LogsDirectory=ares
LogsDirectoryMode=0755
[Install]
WantedBy=multi-user.target
+20
View File
@@ -0,0 +1,20 @@
[Unit]
Description=Hermes Agent Gateway (Telegram)
After=network.target mt5-bridge.service
Wants=mt5-bridge.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/hermes-agent
TimeoutStopSec=210
Environment="HOME=/root"
Environment="PATH=/root/.local/bin:/opt/hermes-agent/.venv-hermes/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/opt/hermes-agent/.venv-hermes/bin/python3 /opt/hermes-agent/hermes gateway run --accept-hooks
Restart=always
RestartSec=15
StandardOutput=append:/var/log/hermes/hermes-gateway.log
StandardError=append:/var/log/hermes/hermes-gateway.log
[Install]
WantedBy=multi-user.target
+14
View File
@@ -0,0 +1,14 @@
model:
default: gpt-4o-mini
provider: custom:openai-compatible
custom_providers:
- name: openai-compatible
base_url: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
memory:
enabled: true
path: /root/.hermes/memory
agent:
max_iterations: 50
shell: bash
restart_drain_timeout: 180
version: 23
+268
View File
@@ -0,0 +1,268 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════════════════════
# GENESIS Trading System — Interactive Setup Wizard
# Powered by API2TRADE · https://app.api2trade.com
# ═══════════════════════════════════════════════════════════════════════════════
set -e
GENESIS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$GENESIS_DIR/.env"
# ── Colours ───────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GRN='\033[0;32m'; YLW='\033[1;33m'
BLU='\033[0;34m'; CYN='\033[0;36m'; WHT='\033[1;37m'; NC='\033[0m'
banner() {
echo ""
echo -e "${CYN} ██████ ███████ ███ ██ ███████ ███████ ██ ███████${NC}"
echo -e "${CYN} ██ ██ ████ ██ ██ ██ ██ ██ ${NC}"
echo -e "${CYN} ██ ███ █████ ██ ██ ██ █████ ███████ ██ ███████${NC}"
echo -e "${CYN} ██ ██ ██ ██ ██ ██ ██ ██ ██ ██${NC}"
echo -e "${CYN} ██████ ███████ ██ ████ ███████ ███████ ██ ███████${NC}"
echo ""
echo -e "${WHT} Autonomous MT5 Trading System — Setup Wizard${NC}"
echo -e "${BLU} Powered by API2TRADE · https://app.api2trade.com${NC}"
echo -e " ─────────────────────────────────────────────────────"
echo ""
}
ok() { echo -e " ${GRN}${NC} $1"; }
warn() { echo -e " ${YLW}${NC} $1"; }
err() { echo -e " ${RED}${NC} $1"; }
hdr() { echo -e "\n ${WHT}$1${NC}"; echo " $(printf '─%.0s' {1..50})"; }
banner
# ── Check if .env already configured ─────────────────────────────────────────
if [ -f "$ENV_FILE" ] && grep -q "^MT5_ACCOUNT_UUID=[a-f0-9-]" "$ENV_FILE" 2>/dev/null; then
echo -e " ${GRN}Existing .env found.${NC}"
echo ""
read -rp " Re-run setup and overwrite? [y/N]: " REDO
[[ "$REDO" =~ ^[Yy]$ ]] || { echo " Skipping setup."; exit 0; }
echo ""
fi
# ═══════════════════════════════════════════════════════════════════════════════
# STEP 1: API2TRADE CREDENTIALS
# ═══════════════════════════════════════════════════════════════════════════════
hdr "Step 1 — API2TRADE Account"
echo ""
echo -e " Sign up at ${BLU}https://app.api2trade.com${NC} if you haven't already."
echo -e " You need: ${WHT}Account UUID${NC} and ${WHT}API Key${NC} from your dashboard."
echo -e " Cost: ${GRN}€12/month per connected MT5 account${NC}"
echo ""
read -rp " API2TRADE Account UUID (from dashboard): " MT5_ACCOUNT_UUID
while [[ ! "$MT5_ACCOUNT_UUID" =~ ^[0-9a-f-]{36}$ ]]; do
err "Invalid UUID format. Should look like: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
read -rp " API2TRADE Account UUID: " MT5_ACCOUNT_UUID
done
read -rp " API2TRADE API Key: " MT5_API_KEY
while [[ ${#MT5_API_KEY} -lt 10 ]]; do
err "API key looks too short. Check your API2TRADE dashboard."
read -rp " API2TRADE API Key: " MT5_API_KEY
done
read -rp " API2TRADE Username (from dashboard): " MT5_API_USER
read -rsp " API2TRADE Password (hidden): " MT5_API_PASS
echo ""
MT5_API_URL="https://mt5.mt4api.dev"
# ── Verify API2TRADE connection ───────────────────────────────────────────────
echo ""
echo " Verifying API2TRADE credentials..."
HTTP_STATUS=$(curl -s -o /tmp/genesis_api_check.json -w "%{http_code}" \
--user "$MT5_API_USER:$MT5_API_PASS" \
"$MT5_API_URL/AccountSummary?id=$MT5_ACCOUNT_UUID" \
--max-time 10 2>/dev/null || echo "000")
if [ "$HTTP_STATUS" = "200" ]; then
BALANCE=$(python3 -c "import json; d=json.load(open('/tmp/genesis_api_check.json')); print(f\"\${d.get('balance',0):,.2f} {d.get('currency','USD')}\")" 2>/dev/null || echo "connected")
ok "API2TRADE connected — Balance: $BALANCE"
else
ERRMSG=$(cat /tmp/genesis_api_check.json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('message',''))" 2>/dev/null || echo "unknown error")
warn "Could not verify connection (HTTP $HTTP_STATUS): $ERRMSG"
warn "Credentials saved — you can verify manually later."
fi
# ═══════════════════════════════════════════════════════════════════════════════
# STEP 2: TELEGRAM BOT
# ═══════════════════════════════════════════════════════════════════════════════
hdr "Step 2 — Telegram Notifications"
echo ""
echo " GENESIS sends trade alerts, P&L reports and heartbeats via Telegram."
echo -e " Create a bot at ${BLU}https://t.me/BotFather${NC} → /newbot → copy the token."
echo -e " Get your Chat ID at ${BLU}https://t.me/userinfobot${NC}"
echo ""
read -rp " Telegram Bot Token (e.g. 123456:ABC-...): " TELEGRAM_BOT_TOKEN
read -rp " Your Telegram Chat ID (numeric, e.g. 123456789): " TELEGRAM_CHAT_ID
# Quick send test
echo ""
echo " Sending test message..."
TG_RESP=$(curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
-d "text=🤖 *GENESIS* setup complete. System is online." \
-d "parse_mode=Markdown" \
--max-time 8 2>/dev/null)
TG_OK=$(echo "$TG_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('ok',''))" 2>/dev/null)
if [ "$TG_OK" = "True" ]; then
ok "Telegram working — check your chat for the test message!"
else
warn "Telegram test failed. Check token and chat ID. (Skipping — you can fix in .env)"
fi
# ═══════════════════════════════════════════════════════════════════════════════
# STEP 3: LLM API KEY (Hermes Brain — OpenAI-Compatible)
# ═══════════════════════════════════════════════════════════════════════════════
hdr "Step 3 — LLM API Key (Hermes Brain)"
echo ""
echo " Hermes uses an LLM to make macro trading decisions every hour."
echo " Supports ANY OpenAI-compatible API provider:"
echo ""
echo -e " ${GRN}1)${NC} OpenAI — https://platform.openai.com/api-keys"
echo -e " ${GRN}2)${NC} DeepSeek — https://platform.deepseek.com"
echo -e " ${GRN}3)${NC} Qwen (Ali) — https://dashscope.aliyun.com"
echo -e " ${GRN}4)${NC} Groq — https://console.groq.com"
echo -e " ${GRN}5)${NC} Together AI — https://api.together.xyz"
echo -e " ${GRN}6)${NC} SiliconFlow — https://cloud.siliconflow.cn"
echo -e " ${GRN}7)${NC} OpenRouter — https://openrouter.ai"
echo -e " ${GRN}8)${NC} Ollama (local)— http://127.0.0.1:11434"
echo -e " ${GRN}0)${NC} Skip — disable LLM brain (strategies still run autonomously)"
echo ""
read -rp " Select provider [1-8, 0 to skip]: " LLM_CHOICE
case "$LLM_CHOICE" in
1) LLM_PROVIDER="openai"; OPENAI_BASE_URL="https://api.openai.com/v1"; DEFAULT_MODEL="gpt-4o-mini" ;;
2) LLM_PROVIDER="deepseek"; OPENAI_BASE_URL="https://api.deepseek.com/v1"; DEFAULT_MODEL="deepseek-chat" ;;
3) LLM_PROVIDER="qwen"; OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"; DEFAULT_MODEL="qwen-plus" ;;
4) LLM_PROVIDER="groq"; OPENAI_BASE_URL="https://api.groq.com/openai/v1"; DEFAULT_MODEL="llama-3.3-70b-versatile" ;;
5) LLM_PROVIDER="together"; OPENAI_BASE_URL="https://api.together.xyz/v1"; DEFAULT_MODEL="meta-llama/Llama-3-70b-chat-hf" ;;
6) LLM_PROVIDER="siliconflow"; OPENAI_BASE_URL="https://api.siliconflow.cn/v1"; DEFAULT_MODEL="deepseek-ai/DeepSeek-V3" ;;
7) LLM_PROVIDER="openrouter"; OPENAI_BASE_URL="https://openrouter.ai/api/v1"; DEFAULT_MODEL="openai/gpt-4o-mini" ;;
8) LLM_PROVIDER="ollama"; OPENAI_BASE_URL="http://127.0.0.1:11434/v1"; DEFAULT_MODEL="llama3" ;;
*) LLM_PROVIDER="none"; OPENAI_BASE_URL=""; DEFAULT_MODEL="" ;;
esac
if [[ "$LLM_PROVIDER" == "none" ]]; then
warn "No LLM provider — Hermes LLM brain disabled. Strategies still run autonomously."
OPENAI_API_KEY="sk-your-api-key-here"
OPENAI_BASE_URL="https://api.openai.com/v1"
HERMES_MODEL="gpt-4o-mini"
else
echo ""
echo -e " Provider: ${GRN}${LLM_PROVIDER}${NC} | Base URL: ${CYN}${OPENAI_BASE_URL}${NC}"
echo -e " Default model: ${WHT}${DEFAULT_MODEL}${NC}"
echo ""
if [[ "$LLM_PROVIDER" == "ollama" ]]; then
OPENAI_API_KEY="ollama"
echo -e " Ollama does not require an API key."
else
read -rp " API Key: " OPENAI_API_KEY
if [[ -z "$OPENAI_API_KEY" ]]; then
warn "No API key entered — Hermes LLM brain disabled."
OPENAI_API_KEY="sk-your-api-key-here"
fi
fi
read -rp " Model name [default: ${DEFAULT_MODEL}]: " HERMES_MODEL
HERMES_MODEL="${HERMES_MODEL:-$DEFAULT_MODEL}"
ok "LLM configured: ${HERMES_MODEL} via ${LLM_PROVIDER}"
fi
# ═══════════════════════════════════════════════════════════════════════════════
# STEP 4: RISK LIMITS
# ═══════════════════════════════════════════════════════════════════════════════
hdr "Step 4 — Risk Management"
echo ""
echo " These limits are enforced by every strategy before any trade."
echo ""
read -rp " Max open positions at once [default: 4]: " MAX_POSITIONS
MAX_POSITIONS="${MAX_POSITIONS:-4}"
read -rp " Max risk per trade as % of balance [default: 0.02 = 2%]: " MAX_RISK_PCT
MAX_RISK_PCT="${MAX_RISK_PCT:-0.02}"
read -rp " Max lot size per trade [default: 3.0]: " MAX_LOTS
MAX_LOTS="${MAX_LOTS:-3.0}"
# ═══════════════════════════════════════════════════════════════════════════════
# WRITE .env
# ═══════════════════════════════════════════════════════════════════════════════
hdr "Writing .env"
cat > "$ENV_FILE" << ENVEOF
# GENESIS Trading System — Environment Configuration
# Generated by setup.sh on $(date -u +"%Y-%m-%d %H:%M UTC")
# ─────────────────────────────────────────────────────────────────────────────
# SECURITY: Never commit this file to git. It is in .gitignore.
# ─────────────────────────────────────────────────────────────────────────────
# ── API2TRADE ─────────────────────────────────────────────────────────────────
# Get these from: https://app.api2trade.com → Dashboard → API Keys
MT5_ACCOUNT_UUID=${MT5_ACCOUNT_UUID}
MT5_ACCOUNT_ID=${MT5_ACCOUNT_UUID}
MT5_API_KEY=${MT5_API_KEY}
MT5_API_USER=${MT5_API_USER}
MT5_API_PASS=${MT5_API_PASS}
MT5_API_URL=${MT5_API_URL}
# ── Telegram ──────────────────────────────────────────────────────────────────
# Create bot: https://t.me/BotFather | Get Chat ID: https://t.me/userinfobot
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
# ── LLM API (Hermes Brain — OpenAI-Compatible) ────────────────────────────────
# Supports: OpenAI, DeepSeek, Qwen, Groq, Together AI, SiliconFlow, OpenRouter, Ollama
OPENAI_API_KEY=${OPENAI_API_KEY}
OPENAI_BASE_URL=${OPENAI_BASE_URL}
HERMES_MODEL=${HERMES_MODEL}
# ── Risk Limits ───────────────────────────────────────────────────────────────
MAX_POSITIONS=${MAX_POSITIONS}
MAX_RISK_PCT=${MAX_RISK_PCT}
MAX_LOTS=${MAX_LOTS}
# ── Optional: Twelve Data (news/economic calendar enrichment) ─────────────────
# Free tier available at: https://twelvedata.com
TWELVE_DATA_API_KEY=
ENVEOF
chmod 600 "$ENV_FILE"
ok ".env written and locked (chmod 600)"
# ═══════════════════════════════════════════════════════════════════════════════
# DONE
# ═══════════════════════════════════════════════════════════════════════════════
echo ""
echo -e " ─────────────────────────────────────────────────────"
echo -e " ${GRN}✓ GENESIS setup complete!${NC}"
echo ""
echo " Next steps:"
echo ""
echo -e " ${WHT}Docker (local test):${NC}"
echo " docker compose up -d"
echo " docker logs -f genesis"
echo ""
echo -e " ${WHT}VPS (production):${NC}"
echo " bash install.sh"
echo ""
echo -e " ${WHT}Run a live scan right now:${NC}"
echo " docker exec -it genesis ares analyze EURUSDxx"
echo " docker exec -it genesis genesis-scan EURUSDxx"
echo ""
echo -e " ${WHT}Watch live logs:${NC}"
echo " docker logs -f genesis"
echo " docker exec -it genesis tail -f /var/log/hermes/autonomous.log"
echo ""
echo -e " ${BLU}API2TRADE dashboard: https://app.api2trade.com${NC}"
echo -e " ─────────────────────────────────────────────────────"
echo ""
+509
View File
@@ -0,0 +1,509 @@
#!/usr/bin/env python3
"""
GENESIS — Apollo Cycle (Strategy C: MA Crossover Trend Following)
Called by apollo_tool.py when Hermes decides to run a trend analysis.
Strategy logic:
- Fast EMA(9) crosses above Slow EMA(21) on M5 → BUY (Golden Cross)
- Fast EMA(9) crosses below Slow EMA(21) on M5 → SELL (Death Cross)
- H1 SMA(50) trend alignment required (only trade in direction of H1 trend)
- ATR-based dynamic SL/TP (adapts to volatility)
- ADX confirms trend strength
- News + session + spread filters (same as Ares)
run_analysis(symbol) → signal dict — NEVER places a trade itself.
"""
import os, json, time, logging, math
from datetime import datetime, timezone
from pathlib import Path
import requests
import yaml
CONFIG_PATH = Path(__file__).parent / "apollo_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "apollo_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(os.getenv("TELEGRAM_CHAT_ID", CFG["telegram"]["chat_id"]))
CACHE_FILE = Path(CFG["cache"]["path"])
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "apollo"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
FAST_MA = int(CFG["indicators"]["fast_ma_period"])
SLOW_MA = int(CFG["indicators"]["slow_ma_period"])
MA_METHOD = CFG["indicators"]["ma_method"]
SIG_TF = CFG["indicators"]["signal_timeframe"]
TREND_TF = CFG["indicators"]["trend_timeframe"]
TREND_MA = int(CFG["indicators"]["trend_ma_period"])
ATR_PERIOD = int(CFG["indicators"]["atr_period"])
RISK_PCT = float(CFG["risk"]["risk_pct"])
MIN_RR = float(CFG["risk"]["min_rr_ratio"])
SL_ATR_MULT = float(CFG["risk"]["sl_atr_multiplier"])
TP_ATR_MULT = float(CFG["risk"]["tp_atr_multiplier"])
MAX_SPREAD = float(CFG["risk"]["max_spread_pips"])
BLOCK_NEWS_MINS = int(CFG["risk"]["block_news_minutes"])
BLOCK_MEDIUM = bool(CFG["risk"]["block_medium_news"])
REQUIRE_TREND = bool(CFG["strictness"]["require_trend_alignment"])
MIN_MA_SEP = float(CFG["strictness"]["min_ma_separation_pct"])
MIN_ADX = float(CFG["strictness"]["min_adx"])
COOLDOWN_SECS = int(CFG["strictness"]["cooldown_seconds"])
START_HOUR = int(CFG["sessions"]["allowed"][0]["start"])
END_HOUR = int(CFG["sessions"]["allowed"][0]["end"])
MAGIC_COMMENT = CFG["strategy"]["comment"]
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/apollo/apollo_cycle.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "apollo"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "apollo_cycle.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
# Cooldown state (in-process; reset on restart — acceptable)
_last_signal_time: dict = {}
# ── Cache ──────────────────────────────────────────────────────────────────────
def load_cache() -> dict:
try:
return json.loads(CACHE_FILE.read_text()) if CACHE_FILE.exists() else {}
except:
return {}
def save_cache(c):
CACHE_FILE.write_text(json.dumps(c))
# ── Mt5Bridge (unified adapter) ────────────────────────────────────────────────
import sys
sys.path.insert(0, str(Path(__file__).parents[2] / "core"))
from mt5_bridge import bridge, get_bars as _bridge_get_bars, pip_size, calc_lot
def tg(msg: str):
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": msg, "parse_mode": "Markdown"},
timeout=10
)
except:
pass
# ── Market data (Mt5Bridge primary, yfinance fallback) ────────────────────────
YF_MAP = {
"EURUSDxx": "EURUSD=X", "GBPUSDxx": "GBPUSD=X", "USDJPYxx": "USDJPY=X",
"XAUUSDxx": "GC=F", "GBPJPYxx": "GBPJPY=X",
"EURUSD": "EURUSD=X", "GBPUSD": "GBPUSD=X", "USDJPY": "USDJPY=X",
"XAUUSD": "GC=F", "GBPJPY": "GBPJPY=X",
}
YF_TF = {"M1": "1m", "M5": "5m", "M15": "15m", "H1": "1h", "H4": "4h", "D1": "1d"}
def get_bars(symbol: str, tf: str = "M5", count: int = 100) -> list:
try:
bars = _bridge_get_bars(symbol, tf, count)
if bars:
return bars
except Exception as e:
log.warning(f"Mt5Bridge get_bars {symbol}/{tf}: {e}, falling back to yfinance")
try:
import yfinance as yf, pandas as pd
yf_sym = YF_MAP.get(symbol, symbol.replace("xx", "=X") if symbol.lower().endswith("xx") else symbol + "=X")
interval = YF_TF.get(tf, "5m")
period = {"1m": "5d", "5m": "5d", "15m": "5d", "1h": "60d",
"4h": "60d", "1d": "365d"}.get(interval, "5d")
df = yf.download(yf_sym, period=period, interval=interval,
progress=False, auto_adjust=True)
if df.empty:
return []
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df.columns = [c.lower() for c in df.columns]
return df.dropna().tail(count).reset_index().to_dict("records")
except Exception as e:
log.error(f"get_bars {symbol}/{tf}: {e}")
return []
# ── Core indicator calculation ─────────────────────────────────────────────────
def compute_ma_crossover(bars: list, fast: int, slow: int,
method: str = "EMA") -> dict:
"""
Compute fast/slow MA and detect crossover on the last two closed candles.
Returns crossover dict with current + previous values.
Crossover detected by comparing [bar -2] vs [bar -1]:
- Use index -3 and -2 (leave -1 as the currently forming candle)
"""
needed = slow + 5
if len(bars) < needed:
return {}
try:
import pandas as pd, ta
df = pd.DataFrame(bars)
df.columns = [c.lower() for c in df.columns]
df["close"] = df["close"].astype(float)
df["high"] = df["high"].astype(float)
df["low"] = df["low"].astype(float)
# MA calculation
if method.upper() == "EMA":
fast_ma = ta.trend.ema_indicator(df["close"], window=fast)
slow_ma = ta.trend.ema_indicator(df["close"], window=slow)
else:
fast_ma = ta.trend.sma_indicator(df["close"], window=fast)
slow_ma = ta.trend.sma_indicator(df["close"], window=slow)
# ADX for trend strength
adx = ta.trend.adx(df["high"], df["low"], df["close"], window=14)
adx_pos = ta.trend.adx_pos(df["high"], df["low"], df["close"], window=14)
adx_neg = ta.trend.adx_neg(df["high"], df["low"], df["close"], window=14)
# ATR for dynamic SL/TP
atr = ta.volatility.average_true_range(
df["high"], df["low"], df["close"], window=ATR_PERIOD)
# MACD for momentum confirmation
macd_hist = ta.trend.macd_diff(df["close"])
def safe(s, i=-2):
try:
v = float(s.iloc[i])
return None if math.isnan(v) else round(v, 6)
except:
return None
# Current = last closed candle (index -2), Prev = one before (index -3)
curr_fast = safe(fast_ma, -2)
curr_slow = safe(slow_ma, -2)
prev_fast = safe(fast_ma, -3)
prev_slow = safe(slow_ma, -3)
if None in (curr_fast, curr_slow, prev_fast, prev_slow):
return {}
# Crossover detection
golden_cross = (prev_fast <= prev_slow) and (curr_fast > curr_slow)
death_cross = (prev_fast >= prev_slow) and (curr_fast < curr_slow)
# MA separation check (filter weak crosses)
separation = abs(curr_fast - curr_slow) / curr_slow if curr_slow > 0 else 0
return {
"curr_fast": curr_fast,
"curr_slow": curr_slow,
"prev_fast": prev_fast,
"prev_slow": prev_slow,
"separation": round(separation, 6),
"golden_cross": golden_cross,
"death_cross": death_cross,
"adx": safe(adx),
"adx_plus": safe(adx_pos),
"adx_minus": safe(adx_neg),
"atr": safe(atr),
"macd_hist": safe(macd_hist),
"close": round(float(df["close"].iloc[-2]), 6),
}
except Exception as e:
log.error(f"compute_ma_crossover: {e}")
return {}
def get_trend_ma(symbol: str) -> float | None:
"""H1 SMA(50) for higher-timeframe trend direction."""
cache = load_cache()
key = f"apollo_h1sma_{symbol}"
now = time.time()
if key in cache and now - cache[key].get("ts", 0) < 300:
return cache[key].get("val")
try:
import pandas as pd, ta
bars = get_bars(symbol, "H1", TREND_MA + 10)
if len(bars) < TREND_MA:
return None
df = pd.DataFrame(bars)
df.columns = [c.lower() for c in df.columns]
df["close"] = df["close"].astype(float)
sma = ta.trend.sma_indicator(df["close"], window=TREND_MA)
val = round(float(sma.iloc[-2]), 6)
cache[key] = {"ts": now, "val": val}
save_cache(cache)
return val
except Exception as e:
log.error(f"get_trend_ma {symbol}: {e}")
return None
# ── Helpers (mirrors ares_cycle.py) ───────────────────────────────────────────
def pip_size(symbol: str) -> float:
if "JPY" in symbol.upper(): return 0.01
if "XAU" in symbol.upper(): return 0.1
return 0.0001
def price_to_pips(diff: float, symbol: str) -> float:
return abs(diff) / pip_size(symbol)
def calculate_lot(equity: float, sl_pips: float, symbol: str) -> float:
risk_eur = equity * RISK_PCT
pip_val = 10.0
if "JPY" in symbol.upper(): pip_val = 9.0
if "GBP" in symbol.upper(): pip_val = 12.5
if "XAU" in symbol.upper(): pip_val = 1.0
raw = risk_eur / (sl_pips * pip_val) if sl_pips > 0 else 0.01
return round(max(0.01, min(round(raw / 0.01) * 0.01, 5.0)), 2)
def check_news_block(symbol: str) -> tuple[bool, list]:
now_utc = datetime.now(timezone.utc)
warnings = []
blocked = set()
cache = load_cache()
for evt in cache.get("ff_cal", {}).get("data", []):
try:
et = datetime.fromisoformat(evt.get("date","")).astimezone(timezone.utc)
mins = (et - now_utc).total_seconds() / 60
imp = evt.get("impact","")
if imp == "High" and -15 < mins < BLOCK_NEWS_MINS:
blocked.add(evt.get("currency","")[:3])
warnings.append(f"High: {evt.get('title')} in {int(mins)}min")
except:
pass
sym_up = symbol.upper()
is_block = any(c and c in sym_up for c in blocked if c)
return is_block, warnings
def is_trade_time() -> bool:
now = datetime.now(timezone.utc)
wd, hr = now.weekday(), now.hour
if (wd == 4 and hr >= 22) or wd == 5 or (wd == 6 and hr < 22):
return False
return START_HOUR <= hr < END_HOUR
def has_apollo_position() -> bool:
pos = bridge("/positions")
if isinstance(pos, list):
for p in pos:
if "APOLLO" in str(p.get("comment", "")).upper():
return True
return False
# ── Main analysis ──────────────────────────────────────────────────────────────
def run_analysis(symbol: str) -> dict:
"""
Full MA Crossover trend-following analysis.
Returns signal dict with action='trade' or action='wait'.
Never places a trade — apollo_tool.py handles execution.
"""
symbol = symbol.upper()
if not symbol.endswith("XX"):
symbol = symbol + "xx"
symbol = symbol[:-2] + "xx"
log.info(f"=== Apollo MA Crossover Analysis: {symbol} ===")
# ── Account ────────────────────────────────────────────────────
account = bridge("/balance")
if "error" in account:
return {"action": "wait", "reason": f"Bridge unreachable: {account['error']}"}
equity = float(account.get("equity", 0))
if equity <= 0:
return {"action": "wait", "reason": "Account equity unavailable."}
# ── Existing Apollo position ───────────────────────────────────
if has_apollo_position():
return {"action": "wait", "reason": "Apollo position already open."}
# ── Cooldown ───────────────────────────────────────────────────
last = _last_signal_time.get(symbol, 0)
if time.time() - last < COOLDOWN_SECS:
remaining = int(COOLDOWN_SECS - (time.time() - last))
return {"action": "wait", "reason": f"Cooldown active: {remaining}s remaining."}
# ── Session ────────────────────────────────────────────────────
if not is_trade_time():
return {"action": "wait",
"reason": f"Outside session (GMT {START_HOUR}:00{END_HOUR}:00)."}
# ── Quote + spread ─────────────────────────────────────────────
quote = bridge(f"/quote?symbol={symbol}")
if "error" in quote or not quote.get("bid"):
return {"action": "wait", "reason": f"No live quote for {symbol}."}
bid = float(quote["bid"])
ask = float(quote["ask"])
spread_pips = price_to_pips(ask - bid, symbol)
if spread_pips > MAX_SPREAD:
return {"action": "wait",
"reason": f"Spread {spread_pips:.2f} pips > max {MAX_SPREAD}."}
# ── News ───────────────────────────────────────────────────────
blocked, news_warn = check_news_block(symbol)
if blocked:
return {"action": "wait",
"reason": f"News block: {'; '.join(news_warn[:2])}"}
# ── M5 bars + MA crossover ─────────────────────────────────────
bars_m5 = get_bars(symbol, SIG_TF, SLOW_MA + 20)
if len(bars_m5) < SLOW_MA + 5:
return {"action": "wait", "reason": "Insufficient M5 bar data."}
ind = compute_ma_crossover(bars_m5, FAST_MA, SLOW_MA, MA_METHOD)
if not ind:
return {"action": "wait", "reason": "MA calculation failed."}
golden = ind["golden_cross"]
death = ind["death_cross"]
if not golden and not death:
return {
"action": "wait",
"reason": (
f"No crossover. Fast={ind['curr_fast']:.5f} "
f"Slow={ind['curr_slow']:.5f} "
f"(prev: {ind['prev_fast']:.5f}/{ind['prev_slow']:.5f})"
)
}
direction = "Buy" if golden else "Sell"
signal_type = "GOLDEN_CROSS" if golden else "DEATH_CROSS"
# ── H1 trend alignment ─────────────────────────────────────────
trend_ma = get_trend_ma(symbol)
close_price = ind["close"]
trend_ok = True
trend_note = "Trend filter skipped (MA unavailable)"
if trend_ma is not None and REQUIRE_TREND:
if direction == "Buy":
trend_ok = close_price > trend_ma
trend_note = (f"H1 SMA{TREND_MA}={trend_ma:.5f}"
f"{'aligned ✓' if trend_ok else 'AGAINST trend ✗'}")
else:
trend_ok = close_price < trend_ma
trend_note = (f"H1 SMA{TREND_MA}={trend_ma:.5f}"
f"{'aligned ✓' if trend_ok else 'AGAINST trend ✗'}")
# ── Confluence checks ──────────────────────────────────────────
adx = ind.get("adx")
sep = ind.get("separation", 0)
mhist = ind.get("macd_hist")
atr = ind.get("atr")
conds = []
if direction == "Buy":
conds = [
(golden, f"Golden Cross: EMA{FAST_MA} crossed above EMA{SLOW_MA}"),
(trend_ok, trend_note),
(adx is not None and adx > MIN_ADX, f"ADX trend strength {adx:.1f} > {MIN_ADX}"),
(sep >= MIN_MA_SEP, f"MA separation {sep*100:.3f}% ≥ {MIN_MA_SEP*100:.3f}%"),
(mhist is not None and mhist > 0, f"MACD histogram positive ({mhist:.6f})"),
]
else:
conds = [
(death, f"Death Cross: EMA{FAST_MA} crossed below EMA{SLOW_MA}"),
(trend_ok, trend_note),
(adx is not None and adx > MIN_ADX, f"ADX trend strength {adx:.1f} > {MIN_ADX}"),
(sep >= MIN_MA_SEP, f"MA separation {sep*100:.3f}% ≥ {MIN_MA_SEP*100:.3f}%"),
(mhist is not None and mhist < 0, f"MACD histogram negative ({mhist:.6f})"),
]
passed = [(m, d) for m, d in conds if m]
failed = [(m, d) for m, d in conds if not m]
# Require at least 4/5 for Apollo (trend-following can be less strict than Ares)
min_conf = 4
if len(passed) < min_conf:
return {
"action": "wait",
"reason": f"Only {len(passed)}/{min_conf} conditions met.",
"conditions_met": [d for _, d in passed],
"conditions_failed": [d for _, d in failed],
}
# ── ATR-based SL/TP ───────────────────────────────────────────
if atr is None or atr <= 0:
atr = 0.001 # fallback
if direction == "Buy":
entry = ask
sl = round(entry - atr * SL_ATR_MULT, 6)
tp = round(entry + atr * TP_ATR_MULT, 6)
else:
entry = bid
sl = round(entry + atr * SL_ATR_MULT, 6)
tp = round(entry - atr * TP_ATR_MULT, 6)
sl_pips = price_to_pips(entry - sl, symbol)
tp_pips = price_to_pips(tp - entry, symbol)
rr = round(tp_pips / sl_pips, 2) if sl_pips > 0 else 0
if rr < MIN_RR:
return {"action": "wait",
"reason": f"R:R {rr} below minimum {MIN_RR}."}
volume = calculate_lot(equity, sl_pips, symbol)
# Update cooldown
_last_signal_time[symbol] = time.time()
reason = (
f"Apollo {signal_type}: EMA{FAST_MA}={ind['curr_fast']:.5f} "
f"{'>' if golden else '<'} EMA{SLOW_MA}={ind['curr_slow']:.5f}. "
f"ADX={adx:.1f}, ATR={atr:.5f}, R:R={rr}, Spread={spread_pips:.2f}pips."
)
log.info(f"SIGNAL: {direction} {symbol} | {signal_type} | SL={sl} TP={tp} Vol={volume}")
return {
"action": "trade",
"strategy": "apollo-ma-crossover",
"signal_type": signal_type,
"symbol": symbol,
"direction": direction,
"entry": entry,
"stop_loss": sl,
"take_profit": tp,
"volume": volume,
"rr_ratio": rr,
"sl_pips": round(sl_pips, 1),
"tp_pips": round(tp_pips, 1),
"confidence": "high" if len(passed) == len(conds) else "medium",
"conditions_met": [d for _, d in passed],
"conditions_failed": [d for _, d in failed],
"warnings": news_warn,
"reason": reason,
"indicators": {
"fast_ma": ind["curr_fast"],
"slow_ma": ind["curr_slow"],
"adx": adx,
"atr": atr,
"macd_hist": mhist,
"h1_trend_ma": trend_ma,
"spread_pips": spread_pips,
},
"analysed_at": datetime.now(timezone.utc).isoformat(),
}
if __name__ == "__main__":
import sys
sym = sys.argv[1] if len(sys.argv) > 1 else "EURUSDxx"
print(f"Running Apollo analysis for {sym}...")
result = run_analysis(sym)
print(json.dumps(result, indent=2, default=str))
+404
View File
@@ -0,0 +1,404 @@
#!/usr/bin/env python3
"""
GENESIS — Apollo Telegram Bot (Strategy C Command Handler)
Same pattern as ares_telegram_bot.py — manual trigger via Telegram.
Commands:
/apollo_analyze [SYMBOL] — MA crossover analysis, no trade
/apollo_execute — Execute pending signal
/apollo_scan — Scan all symbols, show best
/apollo_skip — Cancel pending signal
/apollo_status — Account + open Apollo position
/apollo_help — All commands
"""
import os, json, time, logging, threading, sys
from datetime import datetime, timezone
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "core"))
from mt5_bridge import bridge as _bridge
CONFIG_PATH = Path(__file__).parent / "apollo_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "apollo_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
import requests
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(os.getenv("TELEGRAM_CHAT_ID", CFG["telegram"]["chat_id"]))
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "apollo"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
STRATEGY = CFG["strategy"]["name"]
COMMENT = CFG["strategy"]["comment"]
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/apollo/apollo_bot.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "apollo"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "apollo_bot.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
_pending: dict = {}
_lock = threading.Lock()
def tg_send(text: str):
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "Markdown"},
timeout=10
)
except Exception as e:
log.error(f"tg_send: {e}")
def tg_updates(offset=0):
try:
r = requests.get(
f"https://api.telegram.org/bot{TG_TOKEN}/getUpdates",
params={"timeout": 30, "offset": offset}, timeout=40
)
return r.json().get("result", [])
except:
return []
def bridge(path, method="GET", data=None):
return _bridge(path, method, data)
def journal_write(entry: dict):
with open(JOURNAL, "a") as f:
f.write(json.dumps(entry) + "\n")
def journal_stats():
wins = losses = 0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t = json.loads(line)
if t.get("result") == "win": wins += 1
if t.get("result") == "loss": losses += 1
except: pass
return wins, losses
# ── Commands ───────────────────────────────────────────────────────────────────
def cmd_help():
fast = CFG["indicators"]["fast_ma_period"]
slow = CFG["indicators"]["slow_ma_period"]
tg_send(
f"🏹 *{STRATEGY} — Strategy C Commands*\n\n"
f"`/apollo_analyze [SYMBOL]` — MA crossover analysis (no trade)\n"
f"`/apollo_scan` — Scan all symbols for best signal\n"
f"`/apollo_execute` — Execute pending signal\n"
f"`/apollo_skip` — Cancel pending signal\n"
f"`/apollo_status` — Open position + journal stats\n"
f"`/apollo_help` — This message\n\n"
f"📊 Strategy: EMA{fast}/EMA{slow} Golden/Death Cross on M5\n"
f"🔖 Tag: `{COMMENT}` | Session: GMT "
f"{CFG['sessions']['allowed'][0]['start']}:00"
f"{CFG['sessions']['allowed'][0]['end']}:00"
)
def cmd_status():
acc = bridge("/balance")
if "error" in acc:
tg_send(f"🔴 *{STRATEGY}*: Bridge unreachable.")
return
positions = bridge("/positions")
pos_str = "None"
all_str = []
if isinstance(positions, list):
for p in positions:
comment = str(p.get("comment", ""))
all_str.append(f"`{p.get('symbol')}` {p.get('orderType')} "
f"{p.get('lots')}lot P&L:€{p.get('profit',0):.2f} [{comment}]")
if "APOLLO" in comment.upper():
pos_str = (f"{p.get('symbol')} {p.get('orderType')} "
f"{p.get('lots')}lot | P&L: €{p.get('profit',0):.2f}")
wins, losses = journal_stats()
with _lock:
pend = (f"🟡 {_pending.get('symbol')} {_pending.get('direction')} "
f"({_pending.get('signal_type','?')})"
if _pending else "None")
all_display = "\n".join(all_str) if all_str else "None"
tg_send(
f"🏹 *{STRATEGY} — Status*\n\n"
f"💰 Balance: €{acc.get('balance',0):.2f} | "
f"Equity: €{acc.get('equity',0):.2f}\n"
f"📈 Apollo Position: {pos_str}\n"
f"📋 Pending Signal: {pend}\n"
f"📒 Journal: {wins}W / {losses}L\n\n"
f"*All Open Positions:*\n{all_display}"
)
def cmd_skip():
with _lock:
if not _pending:
tg_send(f"🏹 *{STRATEGY}*: No pending signal to cancel.")
return
sym = _pending.get("symbol")
_pending.clear()
tg_send(f"⏭ *{STRATEGY}*: Signal for `{sym}` cancelled.")
def cmd_execute():
with _lock:
if not _pending:
tg_send(f"🏹 *{STRATEGY}*: No pending signal.\n"
f"Run `/apollo_analyze SYMBOL` or `/apollo_scan` first.")
return
signal = dict(_pending)
_pending.clear()
sym = signal.get("symbol")
dire = signal.get("direction")
sl = signal.get("stop_loss")
tp = signal.get("take_profit")
vol = signal.get("volume", 0.01)
if not all([sym, dire, sl, tp]):
tg_send(f"🏹 *{STRATEGY}*: Incomplete signal — cannot execute.")
return
positions = bridge("/positions")
if isinstance(positions, list) and any(
"APOLLO" in str(p.get("comment","")).upper() for p in positions
):
tg_send(f"⚠️ *{STRATEGY}*: Apollo position already open. Close it first.")
return
tg_send(f"🏹 *{STRATEGY}*: Placing order…")
order = bridge("/market", "POST", {
"symbol": sym, "volume": vol, "type": dire,
"stop_loss": sl, "take_profit": tp, "comment": COMMENT
})
ticket = order.get("ticket") or order.get("Ticket")
if ticket:
now = datetime.now(timezone.utc)
journal_write({
"ticket": str(ticket), "symbol": sym, "direction": dire,
"volume": vol, "sl": sl, "tp": tp,
"signal_type": signal.get("signal_type"),
"rr": signal.get("rr_ratio"),
"opened": now.isoformat(), "result": None, "pnl": None,
"strategy": "apollo-ma-crossover"
})
ind = signal.get("indicators", {})
tg_send(
f"✅ *{STRATEGY} TRADE PLACED*\n"
f"📈 `{sym}` {dire} | {signal.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{signal.get('entry')}` | SL: `{sl}` | TP: `{tp}`\n"
f"R:R: `{signal.get('rr_ratio')}` | Vol: `{vol}`\n"
f"ADX: {ind.get('adx','?')} | ATR: {ind.get('atr','?')}\n"
f"🔖 Ticket: `{ticket}`"
)
else:
err = order.get("message", str(order))
tg_send(f"❌ *{STRATEGY}*: Order FAILED — `{err}`")
def cmd_analyze(symbol: str):
sym = symbol.upper().strip()
if not sym.endswith("XX"):
sym = sym + "xx"
tg_send(f"🏹 *{STRATEGY}*: Analysing `{sym}`… (3060s)")
try:
import importlib.util
spec = importlib.util.spec_from_file_location(
"apollo_cycle", Path(__file__).parent / "apollo_cycle.py"
)
mod = importlib.util.load_from_spec(spec)
spec.loader.exec_module(mod)
result = mod.run_analysis(sym)
except Exception as e:
log.error(f"Analysis error: {e}", exc_info=True)
tg_send(f"❌ *{STRATEGY}*: Analysis failed — `{str(e)[:200]}`")
return
_send_analysis_result(result)
def cmd_scan():
tg_send(f"🏹 *{STRATEGY}*: Scanning all symbols… (may take 6090s)")
try:
import importlib.util
spec = importlib.util.spec_from_file_location(
"apollo_cycle", Path(__file__).parent / "apollo_cycle.py"
)
mod = importlib.util.load_from_spec(spec)
spec.loader.exec_module(mod)
symbols = CFG["symbols"]
best = None
best_rr = 0
scan_lines = []
for sym in symbols:
r = mod.run_analysis(sym)
action = r.get("action", "wait")
if action == "trade":
rr = r.get("rr_ratio", 0) or 0
icon = "🟢"
scan_lines.append(
f"{icon} `{sym}`: {r.get('direction')} "
f"{r.get('signal_type','').replace('_',' ')} | "
f"R:R {rr} | {r.get('confidence','?')}"
)
if rr > best_rr:
best_rr = rr
best = r
else:
scan_lines.append(f"⚪ `{sym}`: {r.get('reason','wait')[:60]}")
import time; time.sleep(0.5)
summary = "\n".join(scan_lines)
tg_send(f"🏹 *{STRATEGY} SCAN RESULTS*\n\n{summary}")
if best:
with _lock:
_pending.clear()
_pending.update(best)
_send_analysis_result(best, from_scan=True)
else:
tg_send(f"📊 *{STRATEGY}*: No trade signals found across all symbols.")
except Exception as e:
log.error(f"Scan error: {e}", exc_info=True)
tg_send(f"❌ *{STRATEGY}*: Scan failed — `{str(e)[:200]}`")
def _send_analysis_result(result: dict, from_scan: bool = False):
"""Format and send analysis result to Telegram, set pending if trade signal."""
action = result.get("action", "wait")
if action != "trade":
tg_send(
f"🏹 *{STRATEGY}* — `{result.get('symbol','?')}`\n\n"
f"📊 Signal: *WAIT*\n"
f"💡 {result.get('reason','')[:300]}"
)
return
with _lock:
_pending.clear()
_pending.update(result)
conds = result.get("conditions_met", [])
cond_str = "\n".join(f"{c}" for c in conds) if conds else " (see reason)"
warn_str = ""
if result.get("warnings"):
warn_str = "\n" + "\n".join(f" ⚠️ {w}" for w in result["warnings"])
ind = result.get("indicators", {})
fast = CFG["indicators"]["fast_ma_period"]
slow = CFG["indicators"]["slow_ma_period"]
scan_note = " _(Best from scan)_" if from_scan else ""
tg_send(
f"🏹 *{STRATEGY} ANALYSIS*{scan_note} — `{result.get('symbol')}`\n\n"
f"📊 Signal: *{result.get('direction')}* — "
f"{result.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{result.get('entry')}`\n"
f"SL: `{result.get('stop_loss')}` ({result.get('sl_pips','?')} pips)\n"
f"TP: `{result.get('take_profit')}` ({result.get('tp_pips','?')} pips)\n"
f"R:R `{result.get('rr_ratio')}` | Vol: `{result.get('volume')}`\n"
f"🎯 Confidence: {result.get('confidence','?')}\n"
f"ADX: {ind.get('adx','?')} | ATR: {ind.get('atr','?')}\n"
f"EMA{fast}: {ind.get('fast_ma','?')} | EMA{slow}: {ind.get('slow_ma','?')}\n\n"
f"📌 *Conditions ({len(conds)} met):*\n{cond_str}{warn_str}\n\n"
f"💡 {result.get('reason','')[:250]}\n\n"
f"Reply `/apollo_execute` to trade or `/apollo_skip` to cancel."
)
# ── Dispatcher ─────────────────────────────────────────────────────────────────
def dispatch(text: str, from_id: str):
if str(from_id) != TG_CHAT_ID:
log.warning(f"Unauthorised: {from_id}")
return
text = text.strip()
lower = text.lower()
if lower.startswith("/apollo_analyze"):
parts = text.split(maxsplit=1)
sym = parts[1] if len(parts) > 1 else ""
if not sym:
tg_send("Usage: `/apollo_analyze EURUSD`")
else:
threading.Thread(target=cmd_analyze, args=(sym,), daemon=True).start()
elif lower == "/apollo_scan":
threading.Thread(target=cmd_scan, daemon=True).start()
elif lower == "/apollo_execute":
threading.Thread(target=cmd_execute, daemon=True).start()
elif lower == "/apollo_skip":
cmd_skip()
elif lower == "/apollo_status":
threading.Thread(target=cmd_status, daemon=True).start()
elif lower in ("/apollo_help", "/apollo"):
cmd_help()
# ── Main loop ──────────────────────────────────────────────────────────────────
def main():
log.info(f"=== {STRATEGY} Telegram Bot started ===")
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/setMyCommands",
json={"commands": [
{"command": "apollo_help", "description": "Show all commands"},
{"command": "apollo_analyze", "description": "MA crossover analysis on symbol"},
{"command": "apollo_scan", "description": "Scan all symbols for best signal"},
{"command": "apollo_execute", "description": "Execute pending signal"},
{"command": "apollo_skip", "description": "Cancel pending signal"},
{"command": "apollo_status", "description": "Position + journal stats"},
]},
timeout=10
)
except Exception as e:
log.warning(f"setMyCommands failed: {e}")
fast = CFG["indicators"]["fast_ma_period"]
slow = CFG["indicators"]["slow_ma_period"]
tg_send(
f"🏹 *{STRATEGY} Bot Online*\n"
f"Strategy C: EMA{fast}/EMA{slow} Trend Following (M5)\n"
f"Send `/apollo_help` to see commands.\n\n"
f"🤖 _Hermes controls this bot autonomously._\n"
f"_You can also trigger manually via the commands above._"
)
offset = 0
while True:
try:
updates = tg_updates(offset)
for upd in updates:
offset = upd["update_id"] + 1
msg = upd.get("message", {})
text = msg.get("text", "")
chat_id = str(msg.get("chat", {}).get("id", ""))
if text.startswith("/apollo"):
dispatch(text, chat_id)
except Exception as e:
log.error(f"Polling error: {e}")
time.sleep(5)
time.sleep(1)
if __name__ == "__main__":
main()
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/env python3
"""
GENESIS Apollo CLI Tool (Strategy C: MA Crossover)
Hermes calls this autonomously same pattern as ares_tool.py.
Usage:
python3 apollo_tool.py analyze EURUSD # MA crossover analysis, no trade
python3 apollo_tool.py execute EURUSD # Analyze + execute if signal found
python3 apollo_tool.py status # Open Apollo position + journal stats
python3 apollo_tool.py close # Close open Apollo position
python3 apollo_tool.py scan # Scan all Apollo symbols, return best signal
python3 apollo_tool.py symbols # List Apollo symbols
"""
import sys, os, json, time
sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent))
from apollo_cycle import (
run_analysis, bridge, JOURNAL, MAGIC_COMMENT,
pip_size, tg, CFG, _last_signal_time
)
from datetime import datetime, timezone
from pathlib import Path
APOLLO_SYMBOLS = CFG["symbols"]
def journal_write(entry: dict):
JOURNAL.parent.mkdir(parents=True, exist_ok=True)
with open(JOURNAL, "a") as f:
f.write(json.dumps(entry) + "\n")
def journal_stats() -> tuple[int, int]:
wins = losses = 0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t = json.loads(line)
if t.get("result") == "win": wins += 1
if t.get("result") == "loss": losses += 1
except: pass
return wins, losses
def cmd_analyze(symbol: str) -> dict:
sym = symbol.upper()
if not sym.endswith("XX"):
sym = sym + "xx"
result = run_analysis(sym)
print(json.dumps(result, indent=2, default=str))
return result
def cmd_execute(symbol: str) -> dict:
"""Analyze + execute if valid signal. Hermes calls when it sees fit."""
sym = symbol.upper()
if not sym.endswith("XX"):
sym = sym + "xx"
result = run_analysis(sym)
if result.get("action") != "trade":
out = {
"executed": False,
"reason": result.get("reason", "No signal"),
"conditions_met": result.get("conditions_met", []),
"signal_type": result.get("signal_type", "none"),
}
print(json.dumps(out, indent=2))
return out
order = bridge("/market", "POST", {
"symbol": result["symbol"],
"volume": result["volume"],
"type": result["direction"],
"stop_loss": result["stop_loss"],
"take_profit": result["take_profit"],
"comment": MAGIC_COMMENT, # "APOLLO-v1"
})
ticket = order.get("ticket") or order.get("Ticket")
now = datetime.now(timezone.utc)
if ticket:
journal_write({
"ticket": str(ticket),
"symbol": result["symbol"],
"direction": result["direction"],
"volume": result["volume"],
"sl": result["stop_loss"],
"tp": result["take_profit"],
"entry": result["entry"],
"rr": result["rr_ratio"],
"signal_type": result["signal_type"],
"opened": now.isoformat(),
"result": None,
"pnl": None,
"strategy": "apollo-ma-crossover",
"triggered_by": "hermes-autonomous",
"conditions": result.get("conditions_met", []),
})
ind = result.get("indicators", {})
tg(
f"🏹 *APOLLO TRADE — Hermes Triggered*\n"
f"📈 `{result['symbol']}` {result['direction']} "
f"| {result['signal_type'].replace('_',' ')}\n"
f"Entry: `{result['entry']}` | SL: `{result['stop_loss']}` "
f"| TP: `{result['take_profit']}`\n"
f"R:R: `{result['rr_ratio']}` | Vol: `{result['volume']}`\n"
f"🎯 Strategy: EMA{CFG['indicators']['fast_ma_period']}/"
f"EMA{CFG['indicators']['slow_ma_period']} Crossover (M5)\n"
f"ADX: {ind.get('adx','?')} | ATR: {ind.get('atr','?')}\n"
f"📌 {', '.join(result.get('conditions_met',[])[:3])}"
)
out = {
"executed": True,
"ticket": str(ticket),
"symbol": result["symbol"],
"direction": result["direction"],
"signal_type": result["signal_type"],
"volume": result["volume"],
"sl": result["stop_loss"],
"tp": result["take_profit"],
"rr": result["rr_ratio"],
"reason": result["reason"],
}
else:
err = order.get("message", str(order))
tg(f"⚠️ *APOLLO*: Order FAILED — `{err}`")
out = {"executed": False, "reason": f"Order failed: {err}"}
print(json.dumps(out, indent=2, default=str))
return out
def cmd_status() -> dict:
acc = bridge("/balance")
positions = bridge("/positions")
apollo_pos = None
all_positions = []
if isinstance(positions, list):
for p in positions:
comment = str(p.get("comment", ""))
all_positions.append({
"ticket": p.get("ticket"),
"symbol": p.get("symbol"),
"type": p.get("orderType"),
"lots": p.get("lots"),
"profit": p.get("profit"),
"comment": comment,
})
if "APOLLO" in comment.upper():
apollo_pos = p
wins, losses = journal_stats()
out = {
"account": acc,
"apollo_position": apollo_pos,
"all_open": all_positions,
"apollo_journal": {"wins": wins, "losses": losses},
"strategy": "MA Crossover EMA9/EMA21 (M5)",
"comment_tag": MAGIC_COMMENT,
}
print(json.dumps(out, indent=2, default=str))
return out
def cmd_close() -> dict:
positions = bridge("/positions")
closed = []
if isinstance(positions, list):
for p in positions:
if "APOLLO" in str(p.get("comment", "")).upper():
res = bridge("/close", "POST", {"ticket": p["ticket"]})
closed.append({"ticket": p["ticket"], "result": res})
tg(f"🏹 *APOLLO*: Position `{p['ticket']}` closed by Hermes.")
out = ({"closed": len(closed), "positions": closed}
if closed else {"closed": 0, "reason": "No open Apollo positions."})
print(json.dumps(out, indent=2, default=str))
return out
def cmd_scan() -> dict:
"""
Scan ALL Apollo symbols and return the best signal found.
Hermes uses this to find the strongest crossover opportunity across all pairs.
"""
best = None
best_score = 0
results = {}
for sym in APOLLO_SYMBOLS:
result = run_analysis(sym)
results[sym] = {
"action": result.get("action"),
"signal_type": result.get("signal_type", "none"),
"confidence": result.get("confidence", "none"),
"rr": result.get("rr_ratio"),
"reason": result.get("reason", "")[:100],
}
if result.get("action") == "trade":
score = (2 if result.get("confidence") == "high" else 1)
score += (result.get("rr_ratio") or 0) * 0.5
score += len(result.get("conditions_met", [])) * 0.3
if score > best_score:
best_score = score
best = result
time.sleep(0.5) # Rate limit yfinance
out = {
"best_signal": best,
"scan_results": results,
"scanned": len(APOLLO_SYMBOLS),
"signals_found": sum(1 for r in results.values() if r["action"] == "trade"),
}
print(json.dumps(out, indent=2, default=str))
return out
def cmd_symbols() -> dict:
out = {
"symbols": APOLLO_SYMBOLS,
"strategy": "MA Crossover Trend Following",
"timeframe": f"{CFG['indicators']['signal_timeframe']} entry, {CFG['indicators']['trend_timeframe']} context",
"indicators": f"EMA{CFG['indicators']['fast_ma_period']}/EMA{CFG['indicators']['slow_ma_period']} crossover",
"comment_tag": MAGIC_COMMENT,
}
print(json.dumps(out, indent=2))
return out
# ── Entry point ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
args = sys.argv[1:]
if not args:
print(json.dumps({"error": "Usage: apollo_tool.py [analyze|execute|status|close|scan|symbols] [SYMBOL]"}))
sys.exit(1)
cmd = args[0].lower()
if cmd == "analyze":
if len(args) < 2:
print(json.dumps({"error": "analyze requires a symbol"})); sys.exit(1)
cmd_analyze(args[1])
elif cmd == "execute":
if len(args) < 2:
print(json.dumps({"error": "execute requires a symbol"})); sys.exit(1)
cmd_execute(args[1])
elif cmd == "status":
cmd_status()
elif cmd == "close":
cmd_close()
elif cmd == "scan":
cmd_scan()
elif cmd == "symbols":
cmd_symbols()
else:
print(json.dumps({"error": f"Unknown command: {cmd}"})); sys.exit(1)
+517
View File
@@ -0,0 +1,517 @@
#!/usr/bin/env python3
"""
GENESIS Ares BB+RSI Mean Reversion Strategy (Strategy B)
Python port of BB_RSI_MeanReversion.mq5 runs via api2trade.com REST API.
Trigger: Called by ares_telegram_bot.py on /ares_analyze
OR by ares_runner.py timer if auto-mode is enabled in config.
NEVER trades autonomously ares_telegram_bot.py gate controls execution.
"""
import os, json, time, logging, math
from datetime import datetime, timezone, timedelta
from pathlib import Path
import requests
import yaml
# ── Config ─────────────────────────────────────────────────────────────────────
CONFIG_PATH = Path(__file__).parent / "ares_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "ares_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(os.getenv("TELEGRAM_CHAT_ID", CFG["telegram"]["chat_id"]))
CACHE_FILE = Path(CFG["cache"]["path"])
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "ares"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
# Strategy parameters — mirroring all EA inputs
BB_PERIOD = 20
BB_DEVIATION = 2.0
RSI_PERIOD = 14
RSI_OVERSOLD = float(CFG["strictness"].get("rsi_oversold", 30))
RSI_OVERBOUGHT = float(CFG["strictness"].get("rsi_overbought", 70))
CONTEXT_MA_PER = int(CFG["strictness"].get("min_adx", 50)) # M15 MA period
CONTEXT_MA_TOL = 0.0002
USE_M15_CONTEXT = CFG["risk"].get("block_medium_news", True)
REQUIRE_OUTSIDE = True # Price must close outside BB
REQUIRE_RSI = True # RSI must confirm
SL_PIPS = int(CFG.get("sl_pips", 20))
TP_PIPS = int(CFG.get("tp_pips", 40))
RISK_PCT = float(CFG["risk"]["risk_pct"])
MIN_RR = float(CFG["risk"]["min_rr_ratio"])
MAX_SPREAD_PIPS = 1.0
BLOCK_NEWS_MINS = int(CFG["risk"]["block_news_minutes"])
START_HOUR = 5 # GMT
END_HOUR = 17 # GMT
MAGIC_COMMENT = CFG["strategy"]["comment"] # "ARES-v1"
MIN_ADX = float(CFG["strictness"]["min_adx"]) # 22
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/ares/ares_cycle.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "ares"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "ares_cycle.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
# ── Cache helpers ──────────────────────────────────────────────────────────────
def load_cache() -> dict:
try:
return json.loads(CACHE_FILE.read_text()) if CACHE_FILE.exists() else {}
except:
return {}
def save_cache(c: dict):
CACHE_FILE.write_text(json.dumps(c))
# ── Mt5Bridge (unified adapter) ────────────────────────────────────────────────
import sys
sys.path.insert(0, str(Path(__file__).parents[2] / "core"))
from mt5_bridge import bridge, get_bars as _bridge_get_bars, pip_size, calc_lot
def mt5api(path, params=None) -> dict | list:
return bridge(path, data=params)
def tg(msg: str):
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": msg, "parse_mode": "Markdown"},
timeout=10
)
except:
pass
# ── Market data (Mt5Bridge primary, yfinance fallback) ─────────────────────────
YF_MAP = {
"EURUSDxx": "EURUSD=X", "GBPUSDxx": "GBPUSD=X", "USDJPYxx": "USDJPY=X",
"XAUUSDxx": "GC=F", "GBPJPYxx": "GBPJPY=X",
"EURUSD": "EURUSD=X", "GBPUSD": "GBPUSD=X", "USDJPY": "USDJPY=X",
"XAUUSD": "GC=F", "GBPJPY": "GBPJPY=X",
}
TF_MAP = {"1m": "M1", "5m": "M5", "15m": "M15", "30m": "M30", "1h": "H1", "4h": "H4", "1d": "D1"}
def get_bars(symbol: str, tf: str = "1m", count: int = 150) -> list:
mt5_tf = TF_MAP.get(tf, "M1")
try:
bars = _bridge_get_bars(symbol, mt5_tf, count)
if bars:
return bars
except Exception as e:
log.warning(f"Mt5Bridge get_bars {symbol}/{tf}: {e}, falling back to yfinance")
try:
import yfinance as yf
import pandas as pd
yf_sym = YF_MAP.get(symbol, symbol.replace("xx", "=X") if symbol.lower().endswith("xx") else symbol + "=X")
period_map = {"1m": "5d", "15m": "5d", "1h": "60d", "4h": "60d", "1d": "365d"}
period = period_map.get(tf, "5d")
df = yf.download(yf_sym, period=period, interval=tf,
progress=False, auto_adjust=True)
if df.empty:
return []
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df.columns = [c.lower() for c in df.columns]
df = df.rename(columns={"adj close": "close"})
df = df.dropna().tail(count).reset_index()
return df.to_dict("records")
except Exception as e:
log.error(f"get_bars {symbol}/{tf}: {e}")
return []
# ── Indicator calculations ─────────────────────────────────────────────────────
def compute_bb_rsi(bars: list, bb_period=20, bb_dev=2.0, rsi_period=14) -> dict:
"""
Compute Bollinger Bands and RSI from raw OHLCV bars.
Returns dict with bb_upper, bb_lower, bb_middle, rsi for the LAST CLOSED bar.
"""
if len(bars) < max(bb_period, rsi_period) + 5:
return {}
try:
import pandas as pd
import ta
df = pd.DataFrame(bars)
df.columns = [c.lower() for c in df.columns]
df["close"] = df["close"].astype(float)
df["high"] = df["high"].astype(float)
df["low"] = df["low"].astype(float)
# Bollinger Bands
bb_upper = ta.volatility.bollinger_hband(df["close"], window=bb_period, window_dev=bb_dev)
bb_lower = ta.volatility.bollinger_lband(df["close"], window=bb_period, window_dev=bb_dev)
bb_mid = ta.volatility.bollinger_mavg(df["close"], window=bb_period)
bb_pct = ta.volatility.bollinger_pband(df["close"], window=bb_period, window_dev=bb_dev)
# RSI
rsi = ta.momentum.rsi(df["close"], window=rsi_period)
# ADX (for trend strength — strictness gate)
adx = ta.trend.adx(df["high"], df["low"], df["close"], window=14)
adx_pos = ta.trend.adx_pos(df["high"], df["low"], df["close"], window=14)
adx_neg = ta.trend.adx_neg(df["high"], df["low"], df["close"], window=14)
# MACD histogram for momentum confirmation
macd_hist = ta.trend.macd_diff(df["close"])
# EMA trend
ema20 = ta.trend.ema_indicator(df["close"], window=20)
ema50 = ta.trend.ema_indicator(df["close"], window=50)
# Use index -2 = last FULLY CLOSED bar (index -1 is current forming)
i = -2
def safe(s):
try:
v = float(s.iloc[i])
return None if math.isnan(v) else round(v, 6)
except:
return None
return {
"bb_upper": safe(bb_upper),
"bb_lower": safe(bb_lower),
"bb_middle": safe(bb_mid),
"bb_pct": safe(bb_pct),
"rsi": safe(rsi),
"adx": safe(adx),
"adx_plus": safe(adx_pos),
"adx_minus": safe(adx_neg),
"macd_hist": safe(macd_hist),
"ema20": safe(ema20),
"ema50": safe(ema50),
"close": round(float(df["close"].iloc[i]), 6),
"trend": "bullish" if (safe(ema20) or 0) > (safe(ema50) or 0) else "bearish",
}
except Exception as e:
log.error(f"compute_bb_rsi: {e}")
return {}
def get_m15_sma(symbol: str, period: int = 50) -> float | None:
"""Get SMA-50 on M15 for context filtering."""
cache = load_cache()
key = f"ares_m15_sma_{symbol}"
now = time.time()
if key in cache and now - cache[key].get("ts", 0) < 300: # 5-min cache
return cache[key].get("val")
try:
import ta, pandas as pd
bars = get_bars(symbol, "15m", period + 10)
if len(bars) < period:
return None
df = pd.DataFrame(bars)
df.columns = [c.lower() for c in df.columns]
df["close"] = df["close"].astype(float)
sma = ta.trend.sma_indicator(df["close"], window=period)
val = round(float(sma.iloc[-2]), 6)
cache[key] = {"ts": now, "val": val}
save_cache(cache)
return val
except Exception as e:
log.error(f"get_m15_sma {symbol}: {e}")
return None
# ── Pip size helper ────────────────────────────────────────────────────────────
def pip_size(symbol: str) -> float:
if "JPY" in symbol.upper():
return 0.01
if "XAU" in symbol.upper() or "GOLD" in symbol.upper():
return 0.1
return 0.0001
def price_to_pips(diff: float, symbol: str) -> float:
return abs(diff) / pip_size(symbol)
# ── Lot size calculation ───────────────────────────────────────────────────────
def calculate_lot(equity: float, sl_pips: int, symbol: str) -> float:
risk_eur = equity * RISK_PCT
# Approximate pip value: €10 per pip per standard lot for EUR pairs
pip_val_per_lot = 10.0
if "JPY" in symbol.upper(): pip_val_per_lot = 9.0
if "GBP" in symbol.upper(): pip_val_per_lot = 12.5
if "XAU" in symbol.upper(): pip_val_per_lot = 1.0 # Gold ~$1/pip/0.01lot
raw_lot = risk_eur / (sl_pips * pip_val_per_lot)
# Clamp and round to 0.01 step
raw_lot = max(0.01, min(raw_lot, 5.0))
return round(round(raw_lot / 0.01) * 0.01, 2)
# ── Spread check ───────────────────────────────────────────────────────────────
def get_spread_pips(quote: dict, symbol: str) -> float:
ask = float(quote.get("ask", 0))
bid = float(quote.get("bid", 0))
return price_to_pips(ask - bid, symbol)
# ── News / calendar block ──────────────────────────────────────────────────────
def check_news_block(symbol: str) -> tuple[bool, list]:
now_utc = datetime.now(timezone.utc)
warnings = []
blocked_ccys = set()
cache = load_cache()
# ForexFactory calendar (shared cache with Hermes)
ff_events = cache.get("ff_cal", {}).get("data", [])
for evt in ff_events:
try:
et = datetime.fromisoformat(evt.get("date", "")).astimezone(timezone.utc)
mins = (et - now_utc).total_seconds() / 60
if evt.get("impact") == "High" and -15 < mins < BLOCK_NEWS_MINS:
blocked_ccys.add(evt.get("currency", "")[:3])
warnings.append(f"High impact: {evt.get('title')} in {int(mins)}min")
except:
pass
sym_up = symbol.upper()
blocked = any(c and c in sym_up for c in blocked_ccys if c)
return blocked, warnings
# ── Session filter ─────────────────────────────────────────────────────────────
def is_trade_time() -> bool:
now_utc = datetime.now(timezone.utc)
wd, hr = now_utc.weekday(), now_utc.hour
# Weekend check
if (wd == 4 and hr >= 22) or wd == 5 or (wd == 6 and hr < 22):
return False
# Session hours (GMT)
return START_HOUR <= hr < END_HOUR
# ── Position check ─────────────────────────────────────────────────────────────
def has_open_position() -> bool:
positions = bridge("/positions")
if isinstance(positions, list):
for p in positions:
comment = str(p.get("comment", ""))
if MAGIC_COMMENT in comment or MAGIC_COMMENT.split("-")[0] in comment:
return True
return False
# ─────────────────────────────────────────────────────────────────────────────
# MAIN ANALYSIS FUNCTION
# Called by ares_telegram_bot.py on /ares_analyze <SYMBOL>
# Returns signal dict — NEVER places a trade itself.
# ─────────────────────────────────────────────────────────────────────────────
def run_analysis(symbol: str) -> dict:
"""
Full BB+RSI mean reversion analysis for `symbol`.
Returns signal dict with action='trade' or action='wait'.
"""
symbol = symbol.upper()
if not symbol.endswith("XX"):
symbol = symbol + "xx"
symbol = symbol[:-2] + "xx"
log.info(f"=== Ares BB+RSI Analysis: {symbol} ===")
# ── 1. Account state ───────────────────────────────────────────
account = bridge("/balance")
if "error" in account:
return {"action": "wait", "reason": f"Bridge unreachable: {account['error']}"}
equity = float(account.get("equity", 0))
balance = float(account.get("balance", 0))
if equity <= 0:
return {"action": "wait", "reason": "Account equity is zero or unavailable."}
# ── 2. Existing position check ─────────────────────────────────
if has_open_position():
return {"action": "wait", "reason": "Ares position already open. Close it first."}
# ── 3. Session filter ──────────────────────────────────────────
if not is_trade_time():
return {"action": "wait", "reason": f"Outside trading session (GMT {START_HOUR}:00{END_HOUR}:00)."}
# ── 4. Live quote + spread ─────────────────────────────────────
quote = bridge(f"/quote?symbol={symbol}")
if "error" in quote or not quote.get("bid"):
return {"action": "wait", "reason": f"No live quote for {symbol}."}
bid = float(quote["bid"])
ask = float(quote["ask"])
spread_pips = get_spread_pips(quote, symbol)
if spread_pips > MAX_SPREAD_PIPS:
return {
"action": "wait",
"reason": f"Spread too wide: {spread_pips:.2f} pips > max {MAX_SPREAD_PIPS} pips."
}
# ── 5. News block ──────────────────────────────────────────────
blocked, news_warnings = check_news_block(symbol)
if blocked:
return {
"action": "wait",
"reason": f"Blocked by news: {'; '.join(news_warnings[:2])}"
}
# ── 6. M1 bars + indicators ────────────────────────────────────
bars_m1 = get_bars(symbol, "1m", 150)
if len(bars_m1) < 50:
return {"action": "wait", "reason": "Insufficient M1 bar data."}
ind = compute_bb_rsi(bars_m1, BB_PERIOD, BB_DEVIATION, RSI_PERIOD)
if not ind:
return {"action": "wait", "reason": "Indicator computation failed."}
close = ind["close"]
bb_upper = ind["bb_upper"]
bb_lower = ind["bb_lower"]
rsi = ind["rsi"]
adx = ind["adx"]
adx_p = ind["adx_plus"]
adx_n = ind["adx_minus"]
mhist = ind["macd_hist"]
if None in (close, bb_upper, bb_lower, rsi):
return {"action": "wait", "reason": "One or more indicator values are None."}
# ── 7. M15 context (SMA50) ─────────────────────────────────────
m15_sma = get_m15_sma(symbol, 50)
ctx_valid_long = True
ctx_valid_short = True
if m15_sma is not None:
ctx_valid_long = close > m15_sma - CONTEXT_MA_TOL
ctx_valid_short = close < m15_sma + CONTEXT_MA_TOL
# ── 8. Signal logic (mirrors MQL5 EA exactly) ──────────────────
long_signal = False
short_signal = False
# Long conditions
bb_long = (close < bb_lower) if REQUIRE_OUTSIDE else True
rsi_long = (rsi < RSI_OVERSOLD) if REQUIRE_RSI else True
long_signal = bb_long and rsi_long and ctx_valid_long
# Short conditions
bb_short = (close > bb_upper) if REQUIRE_OUTSIDE else True
rsi_short = (rsi > RSI_OVERBOUGHT) if REQUIRE_RSI else True
short_signal = bb_short and rsi_short and ctx_valid_short
if not long_signal and not short_signal:
return {
"action": "wait",
"reason": (
f"No signal. Close={close:.5f} | "
f"BB=[{bb_lower:.5f}, {bb_upper:.5f}] | RSI={rsi:.1f}"
)
}
# ── 9. Strictness confluence (Strategy B extra gate) ──────────
# ADX confirms trend momentum exists
adx_ok = adx is not None and adx > MIN_ADX
conditions_met = []
conditions_failed = []
if long_signal:
direction = "Buy"
entry = ask
sl = round(ask - SL_PIPS * pip_size(symbol), 6)
tp = round(ask + TP_PIPS * pip_size(symbol), 6)
conds = [
(close < bb_lower, f"Price below lower BB ({close:.5f} < {bb_lower:.5f})"),
(rsi < RSI_OVERSOLD, f"RSI oversold ({rsi:.1f} < {RSI_OVERSOLD})"),
(ctx_valid_long, f"M15 SMA50 context valid (price above SMA-tol)"),
(adx_ok, f"ADX momentum ({adx:.1f} > {MIN_ADX})"),
(mhist is not None and mhist > -0.0001,
f"MACD histogram not strongly bearish ({mhist:.6f})"),
]
else:
direction = "Sell"
entry = bid
sl = round(bid + SL_PIPS * pip_size(symbol), 6)
tp = round(bid - TP_PIPS * pip_size(symbol), 6)
conds = [
(close > bb_upper, f"Price above upper BB ({close:.5f} > {bb_upper:.5f})"),
(rsi > RSI_OVERBOUGHT, f"RSI overbought ({rsi:.1f} > {RSI_OVERBOUGHT})"),
(ctx_valid_short, f"M15 SMA50 context valid (price below SMA+tol)"),
(adx_ok, f"ADX momentum ({adx:.1f} > {MIN_ADX})"),
(mhist is not None and mhist < 0.0001,
f"MACD histogram not strongly bullish ({mhist:.6f})"),
]
for met, desc in conds:
(conditions_met if met else conditions_failed).append(desc)
min_confluence = int(CFG["strictness"]["min_confluence_count"])
if len(conditions_met) < min_confluence:
return {
"action": "wait",
"reason": f"Only {len(conditions_met)}/{min_confluence} conditions met.",
"conditions_met": conditions_met,
"conditions_failed": conditions_failed,
}
# ── 10. R:R gate ───────────────────────────────────────────────
sl_pips_val = price_to_pips(entry - sl, symbol)
tp_pips_val = price_to_pips(tp - entry, symbol)
rr = round(tp_pips_val / sl_pips_val, 2) if sl_pips_val > 0 else 0
if rr < MIN_RR:
return {"action": "wait", "reason": f"R:R {rr} below minimum {MIN_RR}."}
# ── 11. Lot size ───────────────────────────────────────────────
volume = calculate_lot(equity, SL_PIPS, symbol)
# ── 12. Build signal ───────────────────────────────────────────
reason = (
f"BB+RSI mean reversion — {len(conditions_met)}/{len(conds)} conditions met. "
f"RSI={rsi:.1f}, BB_pct={ind.get('bb_pct', '?')}, ADX={adx:.1f}, "
f"Spread={spread_pips:.2f}pips, R:R={rr}."
)
log.info(f"SIGNAL: {direction} {symbol} | Entry={entry} SL={sl} TP={tp} Vol={volume} RR={rr}")
return {
"action": "trade",
"symbol": symbol,
"direction": direction,
"entry": entry,
"stop_loss": sl,
"take_profit": tp,
"volume": volume,
"rr_ratio": rr,
"sl_pips": round(sl_pips_val, 1),
"tp_pips": round(tp_pips_val, 1),
"confidence": "high" if len(conditions_met) >= min_confluence + 1 else "medium",
"conditions_met": conditions_met,
"conditions_failed": conditions_failed,
"warnings": news_warnings,
"reason": reason,
"indicators": {
"close": close, "bb_upper": bb_upper, "bb_lower": bb_lower,
"rsi": rsi, "adx": adx, "macd_hist": mhist,
"m15_sma50": m15_sma, "spread_pips": spread_pips,
},
"analysed_at": datetime.now(timezone.utc).isoformat(),
}
# ── CLI test mode ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
sym = sys.argv[1] if len(sys.argv) > 1 else "EURUSDxx"
print(f"Running Ares analysis for {sym}...")
result = run_analysis(sym)
print(json.dumps(result, indent=2, default=str))
+357
View File
@@ -0,0 +1,357 @@
#!/usr/bin/env python3
"""
GENESIS Ares Telegram Bot (Strategy B Command Handler)
Listens for your manual commands. Ares NEVER trades on its own.
Commands:
/ares_analyze [SYMBOL] Run full analysis, no trade placed
/ares_execute Execute the last analysis signal (Account B only)
/ares_skip Cancel the pending signal
/ares_status Account B open position + journal stats
/ares_help Show all commands
"""
import os, json, time, logging, requests, threading, sys
from datetime import datetime, timezone
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "core"))
from mt5_bridge import bridge as _bridge
# ── Config ────────────────────────────────────────────────────────────────────
CONFIG_PATH = Path(__file__).parent / "ares_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "ares_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(os.getenv("TELEGRAM_CHAT_ID", CFG["telegram"]["chat_id"]))
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "ares"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
STRATEGY = CFG["strategy"]["name"]
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/ares/ares_bot.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "ares"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "ares_bot.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
# ── Pending signal state (in-memory, one at a time) ───────────────────────────
_pending: dict = {} # Holds last analysis result awaiting /ares_execute
_lock = threading.Lock()
# ── Telegram helpers ──────────────────────────────────────────────────────────
def tg_send(text: str):
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "Markdown"},
timeout=10
)
except Exception as e:
log.error(f"tg_send: {e}")
def tg_updates(offset=0):
try:
r = requests.get(
f"https://api.telegram.org/bot{TG_TOKEN}/getUpdates",
params={"timeout": 30, "offset": offset},
timeout=40
)
return r.json().get("result", [])
except:
return []
# ── Bridge helper (Account B) ─────────────────────────────────────────────────
def bridge(path, method="GET", data=None):
return _bridge(path, method, data)
# ── Journal helpers ───────────────────────────────────────────────────────────
def journal_write(entry: dict):
with open(JOURNAL, "a") as f:
f.write(json.dumps(entry) + "\n")
def journal_stats():
wins = losses = 0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t = json.loads(line)
if t.get("result") == "win": wins += 1
if t.get("result") == "loss": losses += 1
except: pass
return wins, losses
# ── Command handlers ──────────────────────────────────────────────────────────
def cmd_help():
tg_send(
f"⚔️ *{STRATEGY} — Strategy B Commands*\n\n"
f"`/ares_analyze [SYMBOL]` — Full analysis (no trade)\n"
f"`/ares_execute` — Execute pending signal on Account B\n"
f"`/ares_skip` — Cancel pending signal\n"
f"`/ares_status` — Account B position + P&L\n"
f"`/ares_help` — This message\n\n"
f"⚠️ _Ares NEVER trades automatically. YOU must always confirm._"
)
def cmd_status():
acc = bridge("/balance")
if "error" in acc:
tg_send(f"🔴 *{STRATEGY}*: Account B bridge unreachable.\n`{acc['error']}`")
return
pos_data = bridge("/positions")
pos_str = "None"
if isinstance(pos_data, list) and pos_data:
p = pos_data[0]
pos_str = (f"{p.get('symbol')} {p.get('orderType')} "
f"{p.get('lots')}lot | P&L: €{p.get('profit', 0):.2f}")
wins, losses = journal_stats()
with _lock:
pending_str = (f"🟡 Pending: {_pending.get('symbol')} {_pending.get('direction')}"
if _pending else "None")
tg_send(
f"⚔️ *{STRATEGY} — Account B Status*\n\n"
f"💰 Balance: €{acc.get('balance', 0):.2f}\n"
f"📊 Equity: €{acc.get('equity', 0):.2f}\n"
f"📈 Open: {pos_str}\n"
f"📋 Pending Signal: {pending_str}\n"
f"📒 Journal: {wins}W / {losses}L"
)
def cmd_skip():
with _lock:
if not _pending:
tg_send(f"⚔️ *{STRATEGY}*: No pending signal to cancel.")
return
sym = _pending.get("symbol")
_pending.clear()
tg_send(f"⏭ *{STRATEGY}*: Signal for `{sym}` cancelled.")
def cmd_execute():
with _lock:
if not _pending:
tg_send(
f"⚔️ *{STRATEGY}*: No pending signal.\n"
f"Run `/ares_analyze [SYMBOL]` first."
)
return
signal = dict(_pending)
_pending.clear()
sym = signal.get("symbol")
dire = signal.get("direction")
sl = signal.get("stop_loss")
tp = signal.get("take_profit")
vol = signal.get("volume", 0.1)
if not all([sym, dire, sl, tp]):
tg_send(f"⚔️ *{STRATEGY}*: Pending signal is incomplete — cannot execute.")
return
# Check Account B still has no open positions
positions = bridge("/positions")
if isinstance(positions, list) and positions:
tg_send(
f"⚠️ *{STRATEGY}*: Account B already has an open position.\n"
f"Close it first before executing a new trade."
)
return
tg_send(f"⚔️ *{STRATEGY}*: Placing order on Account B…")
order = bridge("/market", "POST", {
"symbol": sym, "volume": vol, "type": dire,
"stop_loss": sl, "take_profit": tp,
"comment": CFG["strategy"]["comment"]
})
log.info(f"Execute order: {order}")
ticket = order.get("ticket") or order.get("Ticket")
if ticket:
now = datetime.now(timezone.utc)
journal_write({
"ticket": str(ticket), "symbol": sym, "direction": dire,
"volume": vol, "sl": sl, "tp": tp,
"opened": now.isoformat(), "result": None, "pnl": None,
"strategy": "ares"
})
tg_send(
f"✅ *{STRATEGY} TRADE PLACED*\n"
f"📈 `{sym}` {dire} | Vol: {vol}\n"
f"SL: {sl} | TP: {tp}\n"
f"🎯 Confidence: {signal.get('confidence', '?')}\n"
f"💡 {signal.get('reason', '')[:200]}\n"
f"🔖 Ticket: `{ticket}`"
)
else:
err = order.get("message", str(order))
tg_send(f"❌ *{STRATEGY}*: Order FAILED — `{err}`")
def cmd_analyze(symbol: str):
"""
Trigger Ares analysis for a given symbol.
Imports ares_cycle.py to run the analysis without placing any trade.
Stores the result in _pending for /ares_execute to act on.
"""
symbol = symbol.upper().strip()
# Ensure symbol has broker suffix
if not symbol.endswith("xx") and not symbol.endswith("XX"):
symbol = symbol + "xx"
tg_send(f"⚔️ *{STRATEGY}*: Analysing `{symbol}`… (this may take 3060s)")
try:
# Import the analysis function from ares_cycle
import importlib.util, sys
spec = importlib.util.spec_from_file_location(
"ares_cycle",
Path(__file__).parent / "ares_cycle.py"
)
mod = importlib.util.load_from_spec(spec)
spec.loader.exec_module(mod)
result = mod.run_analysis(symbol) # Returns signal dict or None
except Exception as e:
log.error(f"Analysis error: {e}", exc_info=True)
tg_send(f"❌ *{STRATEGY}*: Analysis failed — `{str(e)[:200]}`")
return
if not result:
tg_send(
f"⚔️ *{STRATEGY}* — `{symbol}`\n\n"
f"📊 Signal: *NO TRADE*\n"
f"Conditions not met for a strict entry."
)
return
with _lock:
_pending.clear()
_pending.update(result)
action = result.get("action", "wait")
if action != "trade":
tg_send(
f"⚔️ *{STRATEGY}* — `{symbol}`\n\n"
f"📊 Signal: *WAIT*\n"
f"💡 {result.get('reason', '')[:300]}"
)
return
conditions = result.get("conditions_met", [])
cond_str = "\n".join(f"{c}" for c in conditions) if conditions else " (see reason)"
warnings = result.get("warnings", [])
warn_str = ("\n" + "\n".join(f" ⚠️ {w}" for w in warnings)) if warnings else ""
rr = result.get("rr_ratio", "?")
tg_send(
f"⚔️ *{STRATEGY} ANALYSIS* — `{symbol}`\n\n"
f"📊 Signal: *{result.get('direction')}*\n"
f"Entry: `{result.get('entry')}`\n"
f"SL: `{result.get('stop_loss')}` ({result.get('sl_pips', '?')} pips)\n"
f"TP: `{result.get('take_profit')}` ({result.get('tp_pips', '?')} pips)\n"
f"R:R `{rr}`\n"
f"Vol: `{result.get('volume')} lot`\n"
f"🎯 Confidence: {result.get('confidence', '?')}\n\n"
f"📌 *Conditions met ({len(conditions)}/{CFG['strictness']['min_confluence_count']} required):*\n"
f"{cond_str}{warn_str}\n\n"
f"💡 {result.get('reason', '')[:300]}\n\n"
f"Reply `/ares_execute` to place on Account B, or `/ares_skip` to cancel."
)
# ── Dispatcher ────────────────────────────────────────────────────────────────
def dispatch(text: str, from_id: str):
"""Only accept commands from the authorised chat."""
if str(from_id) != TG_CHAT_ID:
log.warning(f"Ignored message from unauthorised ID: {from_id}")
return
text = text.strip()
lower = text.lower()
if lower.startswith("/ares_analyze"):
parts = text.split(maxsplit=1)
sym = parts[1] if len(parts) > 1 else ""
if not sym:
tg_send("Usage: `/ares_analyze EURUSD`")
else:
# Run in thread so bot stays responsive
threading.Thread(target=cmd_analyze, args=(sym,), daemon=True).start()
elif lower == "/ares_execute":
threading.Thread(target=cmd_execute, daemon=True).start()
elif lower == "/ares_skip":
cmd_skip()
elif lower == "/ares_status":
threading.Thread(target=cmd_status, daemon=True).start()
elif lower in ("/ares_help", "/ares"):
cmd_help()
# ── Main polling loop ─────────────────────────────────────────────────────────
def main():
log.info(f"=== {STRATEGY} Telegram Bot started ===")
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/setMyCommands",
json={"commands": [
{"command": "ares_help", "description": "Show all commands"},
{"command": "ares_analyze", "description": "Run analysis on symbol"},
{"command": "ares_execute", "description": "Execute pending signal"},
{"command": "ares_skip", "description": "Cancel pending signal"},
{"command": "ares_status", "description": "Position + journal stats"},
]},
timeout=10
)
except Exception as e:
log.warning(f"setMyCommands failed: {e}")
tg_send(
f"⚔️ *{STRATEGY} Bot Online*\n"
f"Strategy B: BB+RSI Mean Reversion (M1)\n"
f"Send `/ares_help` to see commands.\n\n"
f"🤖 _Hermes controls this bot autonomously._\n"
f"_You can also trigger manually via the commands above._"
)
offset = 0
while True:
try:
updates = tg_updates(offset)
for upd in updates:
offset = upd["update_id"] + 1
msg = upd.get("message", {})
text = msg.get("text", "")
chat_id = str(msg.get("chat", {}).get("id", ""))
if text.startswith("/ares"):
dispatch(text, chat_id)
except Exception as e:
log.error(f"Polling error: {e}")
time.sleep(5)
time.sleep(1)
if __name__ == "__main__":
main()
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""
GENESIS Ares Strategy B CLI Tool
Called by Hermes autonomously as a shell command.
Usage:
python3 ares_tool.py analyze EURUSD # Run BB+RSI analysis, returns JSON
python3 ares_tool.py execute EURUSD # Analyze + auto-execute if signal found
python3 ares_tool.py status # Account state + open Ares position
python3 ares_tool.py close # Close any open Ares position
python3 ares_tool.py symbols # List tradeable symbols for Ares
Hermes uses this tool independently completely separate from trading_cycle.py.
Ares uses: BB(20,2) + RSI(14) mean reversion on M1 with M15 SMA50 context.
"""
import sys, os, json, yaml
sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent))
from ares_cycle import run_analysis, bridge, JOURNAL, MAGIC_COMMENT, pip_size, tg
from datetime import datetime, timezone
from pathlib import Path
_CONFIG_PATH = Path(__file__).parent / "ares_config.yaml"
if not _CONFIG_PATH.exists():
_CONFIG_PATH = Path(__file__).parents[2] / "configs" / "ares_config.yaml"
with open(_CONFIG_PATH, encoding="utf-8") as _f:
_CFG = yaml.safe_load(_f)
ARES_SYMBOLS = _CFG.get("symbols", ["EURUSDxx", "XAUUSDxx", "GBPUSDxx", "USDJPYxx", "GBPJPYxx"])
def journal_write(entry: dict):
JOURNAL.parent.mkdir(parents=True, exist_ok=True)
with open(JOURNAL, "a") as f:
f.write(json.dumps(entry) + "\n")
def cmd_analyze(symbol: str) -> dict:
"""Run full Ares analysis. Returns signal dict."""
result = run_analysis(symbol.upper())
print(json.dumps(result, indent=2, default=str))
return result
def cmd_execute(symbol: str) -> dict:
"""
Analyze + execute if signal found. Hermes calls this when it decides
the Ares strategy conditions are right. Trades on the main account
with ARES-v1 comment tag so it's distinguishable from Hermes trades.
"""
result = run_analysis(symbol.upper())
if result.get("action") != "trade":
print(json.dumps({
"executed": False,
"reason": result.get("reason", "No signal"),
"conditions_met": result.get("conditions_met", []),
}, indent=2))
return result
# Place the order
order = bridge("/market", "POST", {
"symbol": result["symbol"],
"volume": result["volume"],
"type": result["direction"],
"stop_loss": result["stop_loss"],
"take_profit": result["take_profit"],
"comment": MAGIC_COMMENT, # "ARES-v1" — distinguishes from GENESIS-v2
})
ticket = order.get("ticket") or order.get("Ticket")
now = datetime.now(timezone.utc)
if ticket:
journal_write({
"ticket": str(ticket),
"symbol": result["symbol"],
"direction": result["direction"],
"volume": result["volume"],
"sl": result["stop_loss"],
"tp": result["take_profit"],
"entry": result["entry"],
"rr": result["rr_ratio"],
"opened": now.isoformat(),
"result": None,
"pnl": None,
"strategy": "ares-bb-rsi",
"triggered_by": "hermes-autonomous",
"conditions": result.get("conditions_met", []),
})
msg = (
f"⚔️ *ARES TRADE — Hermes Triggered*\n"
f"📈 `{result['symbol']}` {result['direction']} | Vol: {result['volume']}\n"
f"Entry: `{result['entry']}` | SL: `{result['stop_loss']}` | TP: `{result['take_profit']}`\n"
f"R:R: `{result['rr_ratio']}` | Confidence: {result['confidence']}\n"
f"🎯 Strategy: BB+RSI Mean Reversion (M1)\n"
f"📌 {', '.join(result.get('conditions_met', [])[:3])}"
)
tg(msg)
output = {
"executed": True,
"ticket": str(ticket),
"symbol": result["symbol"],
"direction": result["direction"],
"volume": result["volume"],
"sl": result["stop_loss"],
"tp": result["take_profit"],
"rr": result["rr_ratio"],
"reason": result["reason"],
}
else:
err = order.get("message", str(order))
output = {"executed": False, "reason": f"Order failed: {err}"}
tg(f"⚠️ *ARES*: Order FAILED — `{err}`")
print(json.dumps(output, indent=2, default=str))
return output
def cmd_status() -> dict:
"""Return current account state and any open Ares position."""
acc = bridge("/balance")
positions = bridge("/positions")
ares_pos = None
if isinstance(positions, list):
for p in positions:
if MAGIC_COMMENT.split("-")[0] in str(p.get("comment", "")):
ares_pos = p
break
# Journal stats
wins = losses = 0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t = json.loads(line)
if t.get("result") == "win": wins += 1
if t.get("result") == "loss": losses += 1
except: pass
output = {
"account": acc,
"ares_position": ares_pos,
"ares_journal": {"wins": wins, "losses": losses},
"strategy": "BB+RSI Mean Reversion M1",
}
print(json.dumps(output, indent=2, default=str))
return output
def cmd_close() -> dict:
"""Close any open Ares position."""
positions = bridge("/positions")
closed = []
if isinstance(positions, list):
for p in positions:
if MAGIC_COMMENT.split("-")[0] in str(p.get("comment", "")):
result = bridge("/close", "POST", {"ticket": p["ticket"]})
closed.append({"ticket": p["ticket"], "result": result})
tg(f"⚔️ *ARES*: Position `{p['ticket']}` closed by Hermes.")
if not closed:
output = {"closed": 0, "reason": "No open Ares positions found."}
else:
output = {"closed": len(closed), "positions": closed}
print(json.dumps(output, indent=2, default=str))
return output
def cmd_symbols() -> dict:
output = {
"symbols": ARES_SYMBOLS,
"description": "Ares BB+RSI strategy — optimised for low-spread majors",
"timeframe": "M1 entry, M15 context",
"strategy": "Mean reversion: price outside Bollinger Band + RSI extreme",
}
print(json.dumps(output, indent=2))
return output
# ── Entry point ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
args = sys.argv[1:]
if not args:
print(json.dumps({"error": "Usage: ares_tool.py [analyze|execute|status|close|symbols] [SYMBOL]"}))
sys.exit(1)
cmd = args[0].lower()
if cmd == "analyze":
if len(args) < 2:
print(json.dumps({"error": "analyze requires a symbol, e.g.: ares_tool.py analyze EURUSD"}))
sys.exit(1)
cmd_analyze(args[1])
elif cmd == "execute":
if len(args) < 2:
print(json.dumps({"error": "execute requires a symbol, e.g.: ares_tool.py execute EURUSD"}))
sys.exit(1)
cmd_execute(args[1])
elif cmd == "status":
cmd_status()
elif cmd == "close":
cmd_close()
elif cmd == "symbols":
cmd_symbols()
else:
print(json.dumps({"error": f"Unknown command: {cmd}. Use: analyze, execute, status, close, symbols"}))
sys.exit(1)
+395
View File
@@ -0,0 +1,395 @@
#!/usr/bin/env python3
"""
GENESIS Artemis Cycle (Strategy E: Ichimoku Kumo Breakout on H1)
Tenkan(9), Kijun(26), Senkou B(52), Displacement(26).
BUY: price > kumo + green cloud + RSI>50 + Chikou above price
SELL: price < kumo + red cloud + RSI<50 + Chikou below price
Never places trades artemis_tool.py handles execution.
"""
import os, json, time, logging, math
from datetime import datetime, timezone
from pathlib import Path
import requests, yaml
CONFIG_PATH = Path(__file__).parent / "artemis_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "artemis_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(os.getenv("TELEGRAM_CHAT_ID", CFG["telegram"]["chat_id"]))
CACHE_FILE = Path(CFG["cache"]["path"])
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "artemis"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
TENKAN_P = int(CFG["ichimoku"]["tenkan_period"])
KIJUN_P = int(CFG["ichimoku"]["kijun_period"])
SENKOU_B_P = int(CFG["ichimoku"]["senkou_b_period"])
DISP = int(CFG["ichimoku"]["displacement"])
CONF_BARS = int(CFG["ichimoku"]["confirmation_bars"])
REQ_COLOR = bool(CFG["ichimoku"]["require_cloud_color_alignment"])
REQ_CHIKOU = bool(CFG["ichimoku"]["require_chikou_confirmation"])
REQ_KIJUN = bool(CFG["ichimoku"]["require_price_above_kijun"])
RSI_P = int(CFG["confirmation"]["rsi_period"])
RSI_BUY = float(CFG["confirmation"]["rsi_buy_threshold"])
RSI_SELL = float(CFG["confirmation"]["rsi_sell_threshold"])
RISK_PCT = float(CFG["risk"]["risk_pct"])
MIN_RR = float(CFG["risk"]["min_rr_ratio"])
TP_MULT = float(CFG["risk"]["tp_multiplier"])
MAX_SPREAD = float(CFG["risk"]["max_spread_pips"])
BLOCK_NEWS = int(CFG["risk"]["block_news_minutes"])
COOLDOWN = int(CFG["strictness"]["cooldown_seconds"])
SIG_TF = CFG["strictness"]["signal_timeframe"]
START_H = int(CFG["sessions"]["allowed"][0]["start"])
END_H = int(CFG["sessions"]["allowed"][0]["end"])
COMMENT = CFG["strategy"]["comment"]
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/artemis/artemis_cycle.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "artemis"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "artemis_cycle.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
_last_sig: dict = {}
def load_cache():
try: return json.loads(CACHE_FILE.read_text()) if CACHE_FILE.exists() else {}
except: return {}
def save_cache(c): CACHE_FILE.write_text(json.dumps(c))
# ── Mt5Bridge (unified adapter) ────────────────────────────────────────────────
import sys
sys.path.insert(0, str(Path(__file__).parents[2] / "core"))
from mt5_bridge import bridge, get_bars as _bridge_get_bars, pip_size, calc_lot
def tg(msg):
try:
requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": msg, "parse_mode": "Markdown"}, timeout=10)
except: pass
def pip_size(sym): return 0.01 if "JPY" in sym else (0.1 if "XAU" in sym else 0.0001)
def to_pips(diff, sym): return abs(diff) / pip_size(sym)
def calculate_lot(equity, sl_pips, sym):
pv = 10.0
if "JPY" in sym: pv = 9.0
if "GBP" in sym: pv = 12.5
if "XAU" in sym: pv = 1.0
raw = (equity * RISK_PCT) / (sl_pips * pv) if sl_pips > 0 else 0.01
return round(max(0.01, min(round(raw/0.01)*0.01, 5.0)), 2)
YF_MAP = {"EURUSDxx":"EURUSD=X","GBPUSDxx":"GBPUSD=X","USDJPYxx":"USDJPY=X",
"XAUUSDxx":"GC=F","GBPJPYxx":"GBPJPY=X",
"EURUSD":"EURUSD=X","GBPUSD":"GBPUSD=X","USDJPY":"USDJPY=X",
"XAUUSD":"GC=F","GBPJPY":"GBPJPY=X"}
YF_TF = {"H1":"1h","H4":"4h","D1":"1d","M5":"5m"}
def get_bars(sym, tf="H1", count=130):
try:
bars = _bridge_get_bars(sym, tf, count)
if bars:
return bars
except Exception as e:
log.warning(f"Mt5Bridge get_bars {sym}/{tf}: {e}, falling back to yfinance")
try:
import yfinance as yf, pandas as pd
ys = YF_MAP.get(sym, sym.replace("xx","=X") if sym.lower().endswith("xx") else sym + "=X")
itv = YF_TF.get(tf, "1h")
per = {"1h":"60d","4h":"60d","1d":"365d","5m":"5d"}.get(itv,"60d")
df = yf.download(ys, period=per, interval=itv, progress=False, auto_adjust=True)
if df.empty: return []
if isinstance(df.columns, pd.MultiIndex): df.columns = df.columns.get_level_values(0)
df.columns = [c.lower() for c in df.columns]
return df.dropna().tail(count).reset_index().to_dict("records")
except Exception as e:
log.error(f"get_bars {sym}/{tf}: {e}"); return []
def compute_ichimoku(bars: list) -> dict:
"""
Compute all 5 Ichimoku components. Returns values for the LAST CLOSED bar.
Senkou Spans are shifted FORWARD by DISP to get the cloud at current price,
we read SpanA/B at index -(DISP+2), which is the value plotted at current bar.
Chikou Span = current close plotted DISP bars back compare to close at -DISP-2.
"""
needed = SENKOU_B_P + DISP + 10
if len(bars) < needed: return {}
try:
import pandas as pd, ta, numpy as np
df = pd.DataFrame(bars)
df.columns = [c.lower() for c in df.columns]
for col in ["close","high","low"]:
df[col] = df[col].astype(float)
def midpoint(h, l, p):
return (h.rolling(p).max() + l.rolling(p).min()) / 2
tenkan = midpoint(df["high"], df["low"], TENKAN_P)
kijun = midpoint(df["high"], df["low"], KIJUN_P)
span_a = ((tenkan + kijun) / 2) # plotted DISP bars ahead
span_b = midpoint(df["high"], df["low"], SENKOU_B_P) # plotted DISP bars ahead
rsi = ta.momentum.rsi(df["close"], window=RSI_P)
atr = ta.volatility.average_true_range(df["high"], df["low"], df["close"], window=14)
adx = ta.trend.adx(df["high"], df["low"], df["close"], window=14)
def s(series, i=-2):
try:
v = float(series.iloc[i])
return None if math.isnan(v) else round(v, 6)
except: return None
# Current cloud = SpanA/B shifted forward DISP bars → read at -(DISP+2) in original series
cloud_idx = -(DISP + 2)
sa_current = s(span_a, cloud_idx)
sb_current = s(span_b, cloud_idx)
# Kumo boundaries at current bar
kumo_top = max(sa_current, sb_current) if sa_current and sb_current else None
kumo_bot = min(sa_current, sb_current) if sa_current and sb_current else None
cloud_color = "green" if (sa_current and sb_current and sa_current > sb_current) else "red"
# Future cloud (next DISP bars — what SpanA/B are NOW vs price)
sa_future = s(span_a, -2) # Will be plotted DISP bars from now
sb_future = s(span_b, -2)
future_color = "green" if (sa_future and sb_future and sa_future > sb_future) else "red"
close_now = s(df["close"], -2)
close_disp = s(df["close"], -(DISP + 2)) # Chikou compare point
# Chikou = current close vs price DISP bars ago
chikou_bullish = (close_now or 0) > (close_disp or 0)
chikou_bearish = (close_now or 0) < (close_disp or 0)
# Confirmation bars: count how many consecutive bars have closed outside kumo
conf_bull = 0
conf_bear = 0
if kumo_top and kumo_bot:
for i in range(2, CONF_BARS + 3):
c = s(df["close"], -i)
kt = max(s(span_a, -(DISP + i)), s(span_b, -(DISP + i)) or 0)
kb = min(s(span_a, -(DISP + i)) or 0, s(span_b, -(DISP + i)) or 0)
if c and kt and c > kt: conf_bull += 1
elif c and kb and c < kb: conf_bear += 1
else: break
return {
"tenkan": s(tenkan),
"kijun": s(kijun),
"span_a_current": sa_current,
"span_b_current": sb_current,
"span_a_future": sa_future,
"span_b_future": sb_future,
"kumo_top": kumo_top,
"kumo_bottom": kumo_bot,
"cloud_color": cloud_color,
"future_color": future_color,
"chikou_bullish": chikou_bullish,
"chikou_bearish": chikou_bearish,
"conf_bars_bull": conf_bull,
"conf_bars_bear": conf_bear,
"rsi": s(rsi),
"atr": s(atr),
"adx": s(adx),
"close": close_now,
"kijun_current": s(kijun, -2),
}
except Exception as e:
log.error(f"compute_ichimoku: {e}"); return {}
def check_news_block(sym):
now = datetime.now(timezone.utc)
blocked, warns = set(), []
for evt in load_cache().get("ff_cal", {}).get("data", []):
try:
et = datetime.fromisoformat(evt.get("date","")).astimezone(timezone.utc)
mins = (et - now).total_seconds() / 60
if evt.get("impact") == "High" and -15 < mins < BLOCK_NEWS:
blocked.add(evt.get("currency","")[:3])
warns.append(f"{evt.get('title')} in {int(mins)}min")
except: pass
return any(c and c in sym.upper() for c in blocked if c), warns
def is_trade_time():
now = datetime.now(timezone.utc)
wd, hr = now.weekday(), now.hour
if (wd==4 and hr>=22) or wd==5 or (wd==6 and hr<22): return False
return START_H <= hr < END_H
def has_artemis_position():
pos = bridge("/positions")
return isinstance(pos, list) and any("ARTEMIS" in str(p.get("comment","")).upper() for p in pos)
def run_analysis(symbol: str) -> dict:
symbol = symbol.upper()
if not symbol.endswith("XX"): symbol += "xx"
symbol = symbol[:-2] + "xx"
log.info(f"=== Artemis Ichimoku H1 Analysis: {symbol} ===")
acc = bridge("/balance")
if "error" in acc: return {"action":"wait","reason":f"Bridge error: {acc['error']}"}
equity = float(acc.get("equity", 0))
if equity <= 0: return {"action":"wait","reason":"No equity."}
if has_artemis_position(): return {"action":"wait","reason":"Artemis position already open."}
if time.time() - _last_sig.get(symbol, 0) < COOLDOWN:
rem = int(COOLDOWN - (time.time() - _last_sig.get(symbol, 0)))
return {"action":"wait","reason":f"Cooldown: {rem}s remaining."}
if not is_trade_time(): return {"action":"wait","reason":f"Outside session (GMT {START_H}{END_H})."}
quote = bridge(f"/quote?symbol={symbol}")
if "error" in quote or not quote.get("bid"): return {"action":"wait","reason":f"No quote for {symbol}."}
bid, ask = float(quote["bid"]), float(quote["ask"])
spread = to_pips(ask - bid, symbol)
if spread > MAX_SPREAD: return {"action":"wait","reason":f"Spread {spread:.2f} > {MAX_SPREAD} pips."}
blocked, news_warn = check_news_block(symbol)
if blocked: return {"action":"wait","reason":f"News block: {'; '.join(news_warn[:2])}"}
bars = get_bars(symbol, SIG_TF, SENKOU_B_P + DISP + 20)
if len(bars) < SENKOU_B_P + DISP + 5: return {"action":"wait","reason":"Insufficient H1 data."}
ind = compute_ichimoku(bars)
if not ind: return {"action":"wait","reason":"Ichimoku calculation failed."}
close = ind["close"]
k_top = ind["kumo_top"]
k_bot = ind["kumo_bottom"]
rsi = ind["rsi"]
atr = ind["atr"]
kijun = ind["kijun_current"]
f_color = ind["future_color"]
c_color = ind["cloud_color"]
if None in (close, k_top, k_bot, rsi): return {"action":"wait","reason":"Indicator values None."}
# ── Signal detection ──────────────────────────────────────────
bull_break = close > k_top
bear_break = close < k_bot
if not bull_break and not bear_break:
return {"action":"wait","reason":f"Price inside Kumo. Close={close:.5f} Kumo=[{k_bot:.5f},{k_top:.5f}]"}
direction = "Buy" if bull_break else "Sell"
# ── Confluence conditions ─────────────────────────────────────
if direction == "Buy":
conds = [
(close > k_top, f"Price above Kumo ({close:.5f} > {k_top:.5f})"),
(f_color == "green", f"Future cloud GREEN (Span A > B ahead)"),
(not REQ_COLOR or c_color == "green", f"Current cloud {c_color}"),
(not REQ_CHIKOU or ind["chikou_bullish"], f"Chikou Span above price ({'+' if ind['chikou_bullish'] else '-'})"),
(rsi > RSI_BUY, f"RSI {rsi:.1f} > {RSI_BUY}"),
(not REQ_KIJUN or (kijun and close > kijun), f"Price above Kijun ({kijun:.5f if kijun else '?'})"),
(ind["conf_bars_bull"] >= CONF_BARS, f"{ind['conf_bars_bull']} bar(s) confirmed above Kumo"),
]
entry = ask
if kijun: sl = round(kijun - 0.0002, 6)
elif atr: sl = round(entry - atr * 1.5, 6)
else: sl = round(entry - 30 * pip_size(symbol), 6)
sl_dist = abs(entry - sl)
tp = round(entry + sl_dist * TP_MULT, 6)
else:
conds = [
(close < k_bot, f"Price below Kumo ({close:.5f} < {k_bot:.5f})"),
(f_color == "red", f"Future cloud RED (Span B > A ahead)"),
(not REQ_COLOR or c_color == "red", f"Current cloud {c_color}"),
(not REQ_CHIKOU or ind["chikou_bearish"], f"Chikou Span below price"),
(rsi < RSI_SELL, f"RSI {rsi:.1f} < {RSI_SELL}"),
(not REQ_KIJUN or (kijun and close < kijun), f"Price below Kijun ({kijun:.5f if kijun else '?'})"),
(ind["conf_bars_bear"] >= CONF_BARS, f"{ind['conf_bars_bear']} bar(s) confirmed below Kumo"),
]
entry = bid
if kijun: sl = round(kijun + 0.0002, 6)
elif atr: sl = round(entry + atr * 1.5, 6)
else: sl = round(entry + 30 * pip_size(symbol), 6)
sl_dist = abs(entry - sl)
tp = round(entry - sl_dist * TP_MULT, 6)
passed = [(m,d) for m,d in conds if m]
failed = [(m,d) for m,d in conds if not m]
if len(passed) < 5:
return {"action":"wait","reason":f"Only {len(passed)}/7 conditions met.",
"conditions_met":[d for _,d in passed],"conditions_failed":[d for _,d in failed]}
sl_pips = to_pips(entry - sl, symbol)
tp_pips = to_pips(tp - entry, symbol)
rr = round(tp_pips / sl_pips, 2) if sl_pips > 0 else 0
if rr < MIN_RR: return {"action":"wait","reason":f"R:R {rr} < minimum {MIN_RR}."}
volume = calculate_lot(equity, sl_pips, symbol)
_last_sig[symbol] = time.time()
log.info(f"SIGNAL: {direction} {symbol} SL={sl} TP={tp} Vol={volume} RR={rr}")
return {
"action": "trade",
"strategy": "artemis-ichimoku-h1",
"signal_type": "KUMO_BREAKOUT_BULLISH" if direction=="Buy" else "KUMO_BREAKOUT_BEARISH",
"symbol": symbol,
"direction": direction,
"entry": entry,
"stop_loss": sl,
"take_profit": tp,
"volume": volume,
"rr_ratio": rr,
"sl_pips": round(sl_pips, 1),
"tp_pips": round(tp_pips, 1),
"confidence": "high" if len(passed)==len(conds) else "medium",
"conditions_met": [d for _,d in passed],
"conditions_failed":[d for _,d in failed],
"warnings": news_warn,
"indicators": {
"tenkan": ind["tenkan"], "kijun": kijun,
"kumo_top": k_top, "kumo_bottom": k_bot,
"cloud_color": c_color, "future_cloud": f_color,
"span_a": ind["span_a_current"], "span_b": ind["span_b_current"],
"rsi": rsi, "atr": atr, "adx": ind.get("adx"),
"chikou_bullish": ind["chikou_bullish"],
"spread_pips": spread,
},
"signal_schema": {
"strategy_id": "ARTEMIS-v1",
"magic_number": CFG["strategy"]["magic_number"],
"risk_percent": RISK_PCT,
"metadata": {
"tenkan_sen": ind["tenkan"], "kijun_sen": kijun,
"senkou_a": ind["span_a_current"], "senkou_b": ind["span_b_current"],
"kumo_top": k_top, "kumo_bottom": k_bot,
"cloud_color": c_color, "rsi": rsi,
}
},
"analysed_at": datetime.now(timezone.utc).isoformat(),
}
if __name__ == "__main__":
import sys
sym = sys.argv[1] if len(sys.argv) > 1 else "EURUSDxx"
print(json.dumps(run_analysis(sym), indent=2, default=str))
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env python3
"""GENESIS — Artemis Tool + Telegram Bot (Strategy E: Ichimoku H1)
Combined into one file for efficiency. Hermes calls artemis_tool.py CLI.
Bot listens for /artemis_* commands.
"""
import sys, os, json, time, logging, threading
sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent))
from datetime import datetime, timezone
from pathlib import Path
import yaml, requests
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "core"))
from mt5_bridge import bridge as _bridge
CONFIG_PATH = Path(__file__).parent / "artemis_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "artemis_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(CFG["telegram"]["chat_id"])
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "artemis"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
STRATEGY = CFG["strategy"]["name"]
COMMENT = CFG["strategy"]["comment"]
SYMBOLS = CFG["symbols"]
logging.basicConfig(
filename=f"/var/log/artemis/artemis_bot.log",
level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
_pending: dict = {}
_lock = threading.Lock()
def tg_send(text):
try:
requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "Markdown"}, timeout=10)
except: pass
def tg_updates(offset=0):
try:
r = requests.get(f"https://api.telegram.org/bot{TG_TOKEN}/getUpdates",
params={"timeout":30,"offset":offset}, timeout=40)
return r.json().get("result",[])
except: return []
def bridge(path, method="GET", data=None):
return _bridge(path, method, data)
def journal_write(entry):
with open(JOURNAL,"a") as f: f.write(json.dumps(entry)+"\n")
def journal_stats():
w=l=0; pnl=0.0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t=json.loads(line)
if t.get("result")=="win": w+=1
if t.get("result")=="loss": l+=1
pnl+=float(t.get("pnl") or 0)
except: pass
return w,l,round(pnl,2)
def _load_cycle():
import importlib.util
spec = importlib.util.spec_from_file_location("artemis_cycle", Path(__file__).parent/"artemis_cycle.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _analyze(sym):
try: return _load_cycle().run_analysis(sym)
except Exception as e: return {"action":"wait","reason":f"Error: {str(e)[:200]}"}
# ── Core commands ──────────────────────────────────────────────────────────────
def do_analyze(symbol):
sym = symbol.upper(); sym = (sym+"xx") if not sym.endswith("XX") else sym
return _analyze(sym)
def do_execute(symbol):
sym = symbol.upper(); sym = (sym+"xx") if not sym.endswith("XX") else sym
result = _analyze(sym)
if result.get("action") != "trade":
return {"executed":False,"reason":result.get("reason"),"conditions_met":result.get("conditions_met",[])}
order = bridge("/market","POST",{
"symbol":result["symbol"],"volume":result["volume"],"type":result["direction"],
"stop_loss":result["stop_loss"],"take_profit":result["take_profit"],"comment":COMMENT
})
ticket = order.get("ticket") or order.get("Ticket")
if ticket:
journal_write({"ticket":str(ticket),"symbol":result["symbol"],"direction":result["direction"],
"volume":result["volume"],"sl":result["stop_loss"],"tp":result["take_profit"],
"entry":result["entry"],"rr":result["rr_ratio"],"signal_type":result.get("signal_type"),
"opened":datetime.now(timezone.utc).isoformat(),"result":None,"pnl":None,
"strategy":"artemis-ichimoku-h1","triggered_by":"hermes-autonomous"})
ind=result.get("indicators",{})
tg_send(f"🏹 *ARTEMIS TRADE — Hermes Triggered*\n"
f"`{result['symbol']}` {result['direction']} | {result.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{result['entry']}` SL: `{result['stop_loss']}` TP: `{result['take_profit']}`\n"
f"R:R: `{result['rr_ratio']}` | Vol: `{result['volume']}`\n"
f"Cloud: {ind.get('cloud_color','?').upper()} | RSI: {ind.get('rsi','?')} | ADX: {ind.get('adx','?')}\n"
f"Ticket: `{ticket}`")
return {"executed":True,"ticket":str(ticket),"direction":result["direction"],
"rr":result["rr_ratio"],"confidence":result.get("confidence")}
else:
err=order.get("message",str(order)); tg_send(f"⚠️ *ARTEMIS*: Order FAILED — `{err}`")
return {"executed":False,"reason":f"Order failed: {err}"}
def do_scan():
best=None; best_rr=0; results={}
for sym in SYMBOLS:
r=_analyze(sym); results[sym]={"action":r.get("action"),"rr":r.get("rr_ratio"),"reason":r.get("reason","")[:80]}
if r.get("action")=="trade":
rr=r.get("rr_ratio") or 0
if rr > best_rr: best_rr=rr; best=r
time.sleep(0.5)
return {"best_signal":best,"scan_results":results,"signals_found":sum(1 for r in results.values() if r["action"]=="trade")}
def do_status():
acc=bridge("/balance"); pos=bridge("/positions")
artemis_pos=None; all_pos=[]
if isinstance(pos,list):
for p in pos:
c=str(p.get("comment",""))
all_pos.append({"ticket":p.get("ticket"),"symbol":p.get("symbol"),"type":p.get("orderType"),"profit":p.get("profit"),"comment":c})
if "ARTEMIS" in c.upper(): artemis_pos=p
w,l,pnl=journal_stats()
return {"account":acc,"artemis_position":artemis_pos,"all_open":all_pos,
"journal":{"wins":w,"losses":l,"pnl":pnl},"strategy":"Ichimoku Kumo Breakout H1"}
def do_close():
pos=bridge("/positions"); closed=[]
if isinstance(pos,list):
for p in pos:
if "ARTEMIS" in str(p.get("comment","")).upper():
bridge("/close","POST",{"ticket":p["ticket"]}); closed.append(p["ticket"])
tg_send(f"🎯 *ARTEMIS*: Position `{p['ticket']}` closed by Hermes.")
return {"closed":len(closed),"tickets":closed} if closed else {"closed":0,"reason":"No open Artemis positions."}
# ── Telegram formatting ────────────────────────────────────────────────────────
def send_result(result, scan=False):
if result.get("action")!="trade":
tg_send(f"🎯 *{STRATEGY}* — `{result.get('symbol','?')}`\n\nSignal: *WAIT*\n💡 {result.get('reason','')[:300]}")
return
with _lock: _pending.clear(); _pending.update(result)
ind = result.get("indicators",{})
cmet = result.get("conditions_met",[])
tg_send(
f"🎯 *{STRATEGY} SIGNAL*{'_(scan best)_' if scan else ''} — `{result.get('symbol')}`\n\n"
f"Signal: *{result.get('direction')}* — {result.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{result.get('entry')}` SL: `{result.get('stop_loss')}` TP: `{result.get('take_profit')}`\n"
f"R:R: `{result.get('rr_ratio')}` | Vol: `{result.get('volume')}`\n"
f"Confidence: {result.get('confidence','?')}\n"
f"Cloud: {ind.get('cloud_color','?').upper()}{ind.get('future_cloud','?').upper()}\n"
f"RSI: {ind.get('rsi','?')} | ADX: {ind.get('adx','?')} | ATR: {ind.get('atr','?')}\n\n"
f"📌 *Conditions ({len(cmet)} met):*\n"
+ "\n".join(f"{c}" for c in cmet[:5]) +
f"\n\n`/artemis_execute` to trade | `/artemis_skip` to cancel"
)
def send_status(r):
acc=r.get("account",{}); ap=r.get("artemis_position"); j=r.get("journal",{})
all_s="\n".join(f"`{p['symbol']}` {p['type']}{p.get('profit',0):.2f} [{p['comment']}]" for p in r.get("all_open",[]))
tg_send(
f"🎯 *{STRATEGY} — Status*\n\n"
f"💰 Balance: €{acc.get('balance',0):.2f} | Equity: €{acc.get('equity',0):.2f}\n"
f"📈 Artemis Position: {ap.get('symbol','None') if ap else 'None'}\n"
f"📒 Journal: {j.get('wins',0)}W / {j.get('losses',0)}L | PnL: €{j.get('pnl',0)}\n\n"
f"*All Open:*\n{all_s or 'None'}"
)
# ── Telegram dispatcher ────────────────────────────────────────────────────────
def dispatch(text, from_id):
if str(from_id)!=TG_CHAT_ID: return
lower=text.lower().strip()
def bg(fn, *args): threading.Thread(target=fn,args=args,daemon=True).start()
if lower.startswith("/artemis_analyze"):
parts=text.split(maxsplit=1)
sym=parts[1] if len(parts)>1 else ""
if not sym: tg_send("Usage: `/artemis_analyze EURUSD`"); return
def run():
tg_send(f"🎯 *{STRATEGY}*: Analysing `{sym.upper()}` on H1… (3060s)")
send_result(do_analyze(sym))
bg(run)
elif lower=="/artemis_scan":
def run():
tg_send(f"🎯 *{STRATEGY}*: Scanning {len(SYMBOLS)} symbols on H1…")
r=do_scan()
lines=[f"{'🟢' if v['action']=='trade' else ''} `{s}`: {v['reason'][:60]}"
for s,v in r["scan_results"].items()]
tg_send("🎯 *ARTEMIS SCAN*\n\n"+"\n".join(lines))
if r["best_signal"]: send_result(r["best_signal"],scan=True)
else: tg_send(f"📊 No signals found across {len(SYMBOLS)} symbols.")
bg(run)
elif lower=="/artemis_execute":
def run():
with _lock:
if not _pending:
tg_send(f"🎯 No pending signal. Run `/artemis_scan` or `/artemis_analyze SYMBOL` first."); return
sig=dict(_pending); _pending.clear()
tg_send(f"🎯 Placing Artemis order…")
r=do_execute(sig.get("symbol","").replace("xx",""))
if not r.get("executed"): tg_send(f"❌ Failed: {r.get('reason')}")
bg(run)
elif lower=="/artemis_skip":
with _lock:
if not _pending: tg_send(f"🎯 No pending signal."); return
sym=_pending.get("symbol"); _pending.clear()
tg_send(f"⏭ *{STRATEGY}*: Signal for `{sym}` cancelled.")
elif lower=="/artemis_status":
bg(lambda: send_status(do_status()))
elif lower in ("/artemis_help","/artemis"):
tg_send(
f"🎯 *{STRATEGY} — Strategy E Commands*\n\n"
f"`/artemis_analyze [SYMBOL]` — Ichimoku H1 analysis\n"
f"`/artemis_scan` — Scan all {len(SYMBOLS)} symbols\n"
f"`/artemis_execute` — Execute pending signal\n"
f"`/artemis_skip` — Cancel pending signal\n"
f"`/artemis_status` — Position + journal\n"
f"`/artemis_help` — This message\n\n"
f"📊 Strategy: Ichimoku Kumo Breakout | H1\n"
f"🔖 Tag: `{COMMENT}` | Risk: 0.75%"
)
# ── CLI mode (called by Hermes via ares_tool.py pattern) ──────────────────────
def cli():
args=sys.argv[1:]
if not args: print(json.dumps({"error":"Usage: artemis_tool.py [analyze|execute|scan|status|close|symbols] [SYMBOL]"})); sys.exit(1)
cmd=args[0].lower()
if cmd=="analyze": print(json.dumps(do_analyze(args[1] if len(args)>1 else "EURUSDxx"),indent=2,default=str))
elif cmd=="execute": print(json.dumps(do_execute(args[1] if len(args)>1 else "EURUSDxx"),indent=2,default=str))
elif cmd=="scan": print(json.dumps(do_scan(),indent=2,default=str))
elif cmd=="status": print(json.dumps(do_status(),indent=2,default=str))
elif cmd=="close": print(json.dumps(do_close(),indent=2,default=str))
elif cmd=="symbols": print(json.dumps({"symbols":SYMBOLS,"strategy":"Ichimoku Kumo Breakout H1","comment":COMMENT},indent=2))
else: print(json.dumps({"error":f"Unknown: {cmd}"}))
# ── Bot mode ───────────────────────────────────────────────────────────────────
def bot():
log.info(f"=== {STRATEGY} Telegram Bot started ===")
tg_send(
f"🎯 *{STRATEGY} Bot Online*\n"
f"Strategy E: Ichimoku Kumo Breakout (H1)\n"
f"Tenkan(9) / Kijun(26) / Senkou B(52)\n"
f"Send `/artemis_help` to see commands.\n\n"
f"🤖 _Hermes controls this bot autonomously._\n"
f"_Manual override available via commands above._"
)
offset=0
while True:
try:
for upd in tg_updates(offset):
offset=upd["update_id"]+1
msg=upd.get("message",{}); text=msg.get("text","")
chat_id=str(msg.get("chat",{}).get("id",""))
if text.startswith("/artemis"): dispatch(text,chat_id)
except Exception as e: log.error(f"Poll error: {e}"); time.sleep(5)
time.sleep(1)
if __name__=="__main__":
# If called as artemis_tool.py → CLI mode
# If called as artemis_telegram_bot.py → bot mode
if Path(sys.argv[0]).name.startswith("artemis_telegram"):
bot()
else:
cli()
+519
View File
@@ -0,0 +1,519 @@
#!/usr/bin/env python3
"""
GENESIS Athena Cycle (Strategy D: BB+RSI Mean Reversion on M5)
Complements Ares (M1 strict) with a faster, simpler 2-condition entry on M5.
Differences from Ares:
- Timeframe: M5 (vs Ares M1) catches intraday mean reversion moves
- Entry: Pure BB + RSI only (no ADX gate, no MACD requirement)
- Risk: 0.5% per trade (vs 1%) more frequent signals, smaller size
- Context: H4 SMA50 (vs Ares M15) broader trend filter
- SL/TP: ATR-based dynamic (vs Ares fixed pips)
run_analysis(symbol) signal dict. Never places trades directly.
"""
import os, json, time, logging, math
from datetime import datetime, timezone
from pathlib import Path
import requests
import yaml
CONFIG_PATH = Path(__file__).parent / "athena_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "athena_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(os.getenv("TELEGRAM_CHAT_ID", CFG["telegram"]["chat_id"]))
CACHE_FILE = Path(CFG["cache"]["path"])
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "athena"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
BB_PERIOD = int(CFG["indicators"]["bb_period"])
BB_DEV = float(CFG["indicators"]["bb_deviation"])
RSI_PERIOD = int(CFG["indicators"]["rsi_period"])
RSI_OS = float(CFG["indicators"]["rsi_oversold"])
RSI_OB = float(CFG["indicators"]["rsi_overbought"])
SIG_TF = CFG["indicators"]["signal_timeframe"]
TREND_TF = CFG["indicators"]["trend_timeframe"]
TREND_MA_PER = int(CFG["indicators"]["trend_ma_period"])
ATR_PERIOD = int(CFG["indicators"]["atr_period"])
RISK_PCT = float(CFG["risk"]["risk_pct"])
MIN_RR = float(CFG["risk"]["min_rr_ratio"])
SL_ATR_MULT = float(CFG["risk"]["sl_atr_multiplier"])
TP_ATR_MULT = float(CFG["risk"]["tp_atr_multiplier"])
MAX_SPREAD = float(CFG["risk"]["max_spread_pips"])
BLOCK_NEWS_MINS = int(CFG["risk"]["block_news_minutes"])
REQUIRE_CLOSE = bool(CFG["strictness"]["require_band_close"])
REQUIRE_CTX = bool(CFG["strictness"]["require_h4_context"])
COOLDOWN_SECS = int(CFG["strictness"]["cooldown_seconds"])
MAX_PER_HOUR = int(CFG["strictness"]["max_signals_per_hour"])
START_HOUR = int(CFG["sessions"]["allowed"][0]["start"])
END_HOUR = int(CFG["sessions"]["allowed"][0]["end"])
MAGIC_COMMENT = CFG["strategy"]["comment"]
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/athena/athena_cycle.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "athena"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "athena_cycle.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
_last_signal_time: dict = {}
_signals_this_hour: dict = {}
# ── Helpers ────────────────────────────────────────────────────────────────────
def load_cache() -> dict:
try:
return json.loads(CACHE_FILE.read_text()) if CACHE_FILE.exists() else {}
except:
return {}
def save_cache(c):
CACHE_FILE.write_text(json.dumps(c))
# ── Mt5Bridge (unified adapter) ────────────────────────────────────────────────
import sys
sys.path.insert(0, str(Path(__file__).parents[2] / "core"))
from mt5_bridge import bridge, get_bars as _bridge_get_bars, pip_size, calc_lot
def tg(msg: str):
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": msg, "parse_mode": "Markdown"},
timeout=10
)
except:
pass
def pip_size(symbol: str) -> float:
if "JPY" in symbol.upper(): return 0.01
if "XAU" in symbol.upper(): return 0.1
return 0.0001
def price_to_pips(diff: float, symbol: str) -> float:
return abs(diff) / pip_size(symbol)
def calculate_lot(equity: float, sl_pips: float, symbol: str) -> float:
risk_eur = equity * RISK_PCT
pip_val = 10.0
if "JPY" in symbol.upper(): pip_val = 9.0
if "GBP" in symbol.upper(): pip_val = 12.5
if "XAU" in symbol.upper(): pip_val = 1.0
raw = risk_eur / (sl_pips * pip_val) if sl_pips > 0 else 0.01
return round(max(0.01, min(round(raw / 0.01) * 0.01, 5.0)), 2)
# ── Market data (Mt5Bridge primary, yfinance fallback) ────────────────────────
YF_MAP = {
"EURUSDxx": "EURUSD=X", "GBPUSDxx": "GBPUSD=X", "USDJPYxx": "USDJPY=X",
"XAUUSDxx": "GC=F", "GBPJPYxx": "GBPJPY=X",
"EURUSD": "EURUSD=X", "GBPUSD": "GBPUSD=X", "USDJPY": "USDJPY=X",
"XAUUSD": "GC=F", "GBPJPY": "GBPJPY=X",
}
YF_TF = {"M1":"1m","M5":"5m","M15":"15m","H1":"1h","H4":"4h","D1":"1d"}
def get_bars(symbol: str, tf: str = "M5", count: int = 120) -> list:
try:
bars = _bridge_get_bars(symbol, tf, count)
if bars:
return bars
except Exception as e:
log.warning(f"Mt5Bridge get_bars {symbol}/{tf}: {e}, falling back to yfinance")
try:
import yfinance as yf, pandas as pd
yf_sym = YF_MAP.get(symbol, symbol.replace("xx","=X") if symbol.lower().endswith("xx") else symbol + "=X")
interval = YF_TF.get(tf, "5m")
period = {"1m":"5d","5m":"5d","15m":"5d","1h":"60d","4h":"60d","1d":"365d"}.get(interval,"5d")
df = yf.download(yf_sym, period=period, interval=interval,
progress=False, auto_adjust=True)
if df.empty: return []
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df.columns = [c.lower() for c in df.columns]
return df.dropna().tail(count).reset_index().to_dict("records")
except Exception as e:
log.error(f"get_bars {symbol}/{tf}: {e}")
return []
# ── Core indicators ────────────────────────────────────────────────────────────
def compute_indicators(bars: list) -> dict:
"""
Compute BB(20,2), RSI(14), ATR(14), OBV, Stochastic.
Uses index -2 (last CLOSED candle, not the forming one).
"""
if len(bars) < BB_PERIOD + 5:
return {}
try:
import pandas as pd, ta
df = pd.DataFrame(bars)
df.columns = [c.lower() for c in df.columns]
for col in ["close","high","low","open"]:
df[col] = df[col].astype(float)
if "volume" not in df.columns:
df["volume"] = 1.0
df["volume"] = df["volume"].astype(float)
# Bollinger Bands
bb_upper = ta.volatility.bollinger_hband(df["close"], window=BB_PERIOD, window_dev=BB_DEV)
bb_lower = ta.volatility.bollinger_lband(df["close"], window=BB_PERIOD, window_dev=BB_DEV)
bb_mid = ta.volatility.bollinger_mavg(df["close"], window=BB_PERIOD)
bb_pct = ta.volatility.bollinger_pband(df["close"], window=BB_PERIOD, window_dev=BB_DEV)
bb_width = ta.volatility.bollinger_wband(df["close"], window=BB_PERIOD, window_dev=BB_DEV)
# RSI
rsi = ta.momentum.rsi(df["close"], window=RSI_PERIOD)
# Stochastic (additional confirmation)
stoch_k = ta.momentum.stoch(df["high"], df["low"], df["close"], window=14)
stoch_d = ta.momentum.stoch_signal(df["high"], df["low"], df["close"], window=14)
# ATR
atr = ta.volatility.average_true_range(df["high"], df["low"], df["close"], window=ATR_PERIOD)
# ADX (soft bonus — not a gate for Athena)
adx = ta.trend.adx(df["high"], df["low"], df["close"], window=14)
adx_pos = ta.trend.adx_pos(df["high"], df["low"], df["close"], window=14)
adx_neg = ta.trend.adx_neg(df["high"], df["low"], df["close"], window=14)
# OBV direction (volume confirmation)
obv = ta.volume.on_balance_volume(df["close"], df["volume"])
# MACD (soft)
macd_hist = ta.trend.macd_diff(df["close"])
def safe(s, i=-2):
try:
v = float(s.iloc[i])
return None if math.isnan(v) else round(v, 6)
except:
return None
# OBV trend: is OBV rising or falling over last 3 candles?
obv_now = safe(obv, -2)
obv_prev = safe(obv, -5)
obv_rising = (obv_now or 0) > (obv_prev or 0)
return {
"bb_upper": safe(bb_upper),
"bb_lower": safe(bb_lower),
"bb_middle": safe(bb_mid),
"bb_pct": safe(bb_pct),
"bb_width": safe(bb_width),
"rsi": safe(rsi),
"stoch_k": safe(stoch_k),
"stoch_d": safe(stoch_d),
"atr": safe(atr),
"adx": safe(adx),
"adx_plus": safe(adx_pos),
"adx_minus": safe(adx_neg),
"obv_rising": obv_rising,
"macd_hist": safe(macd_hist),
"close": round(float(df["close"].iloc[-2]), 6),
"high": round(float(df["high"].iloc[-2]), 6),
"low": round(float(df["low"].iloc[-2]), 6),
}
except Exception as e:
log.error(f"compute_indicators: {e}")
return {}
def get_h4_context(symbol: str) -> float | None:
"""H4 SMA50 for broad trend direction."""
cache = load_cache()
key = f"athena_h4sma_{symbol}"
now = time.time()
if key in cache and now - cache[key].get("ts", 0) < 900: # 15min cache
return cache[key].get("val")
try:
import pandas as pd, ta
bars = get_bars(symbol, "H4", TREND_MA_PER + 10)
if len(bars) < TREND_MA_PER: return None
df = pd.DataFrame(bars)
df.columns = [c.lower() for c in df.columns]
df["close"] = df["close"].astype(float)
sma = ta.trend.sma_indicator(df["close"], window=TREND_MA_PER)
val = round(float(sma.iloc[-2]), 6)
cache[key] = {"ts": now, "val": val}
save_cache(cache)
return val
except Exception as e:
log.error(f"get_h4_context {symbol}: {e}")
return None
def check_news_block(symbol: str) -> tuple[bool, list]:
now_utc = datetime.now(timezone.utc)
warnings = []
blocked = set()
cache = load_cache()
for evt in cache.get("ff_cal", {}).get("data", []):
try:
et = datetime.fromisoformat(evt.get("date","")).astimezone(timezone.utc)
mins = (et - now_utc).total_seconds() / 60
if evt.get("impact") == "High" and -15 < mins < BLOCK_NEWS_MINS:
blocked.add(evt.get("currency","")[:3])
warnings.append(f"High: {evt.get('title')} in {int(mins)}min")
except:
pass
sym_up = symbol.upper()
is_block = any(c and c in sym_up for c in blocked if c)
return is_block, warnings
def is_trade_time() -> bool:
now = datetime.now(timezone.utc)
wd, hr = now.weekday(), now.hour
if (wd == 4 and hr >= 22) or wd == 5 or (wd == 6 and hr < 22):
return False
return START_HOUR <= hr < END_HOUR
def check_rate_limit(symbol: str) -> tuple[bool, str]:
"""Check cooldown + hourly rate limit."""
now = time.time()
# Per-symbol cooldown
if now - _last_signal_time.get(symbol, 0) < COOLDOWN_SECS:
remaining = int(COOLDOWN_SECS - (now - _last_signal_time.get(symbol, 0)))
return False, f"Cooldown: {remaining}s remaining for {symbol}"
# Hourly rate limit (across all symbols)
hour_key = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H")
count = _signals_this_hour.get(hour_key, 0)
if count >= MAX_PER_HOUR:
return False, f"Rate limit: {count}/{MAX_PER_HOUR} signals this hour"
return True, ""
def has_athena_position() -> bool:
pos = bridge("/positions")
if isinstance(pos, list):
for p in pos:
if "ATHENA" in str(p.get("comment","")).upper():
return True
return False
# ── Main analysis ──────────────────────────────────────────────────────────────
def run_analysis(symbol: str) -> dict:
"""
Full Athena BB+RSI mean reversion analysis on M5.
Returns signal dict. Never executes athena_tool.py handles that.
"""
symbol = symbol.upper()
if not symbol.endswith("XX"):
symbol = symbol + "xx"
symbol = symbol[:-2] + "xx"
log.info(f"=== Athena BB+RSI M5 Analysis: {symbol} ===")
# ── Account ─────────────────────────────────────────────────────
account = bridge("/balance")
if "error" in account:
return {"action":"wait","reason":f"Bridge unreachable: {account['error']}"}
equity = float(account.get("equity", 0))
if equity <= 0:
return {"action":"wait","reason":"Account equity unavailable."}
# ── Existing Athena position ─────────────────────────────────────
if has_athena_position():
return {"action":"wait","reason":"Athena position already open."}
# ── Rate limits ──────────────────────────────────────────────────
ok, reason = check_rate_limit(symbol)
if not ok:
return {"action":"wait","reason":reason}
# ── Session ──────────────────────────────────────────────────────
if not is_trade_time():
return {"action":"wait","reason":f"Outside session (GMT {START_HOUR}{END_HOUR})."}
# ── Quote + spread ───────────────────────────────────────────────
quote = bridge(f"/quote?symbol={symbol}")
if "error" in quote or not quote.get("bid"):
return {"action":"wait","reason":f"No live quote for {symbol}."}
bid = float(quote["bid"])
ask = float(quote["ask"])
spread_pips = price_to_pips(ask - bid, symbol)
if spread_pips > MAX_SPREAD:
return {"action":"wait","reason":f"Spread {spread_pips:.2f} > max {MAX_SPREAD} pips."}
# ── News block ───────────────────────────────────────────────────
blocked, news_warn = check_news_block(symbol)
if blocked:
return {"action":"wait","reason":f"News block: {'; '.join(news_warn[:2])}"}
# ── M5 indicators ────────────────────────────────────────────────
bars = get_bars(symbol, SIG_TF, BB_PERIOD + 30)
if len(bars) < BB_PERIOD + 5:
return {"action":"wait","reason":"Insufficient M5 bar data."}
ind = compute_indicators(bars)
if not ind:
return {"action":"wait","reason":"Indicator computation failed."}
close = ind["close"]
bb_upper = ind["bb_upper"]
bb_lower = ind["bb_lower"]
rsi = ind["rsi"]
atr = ind.get("atr")
adx = ind.get("adx")
stoch_k = ind.get("stoch_k")
macd_hist= ind.get("macd_hist")
obv_up = ind.get("obv_rising", True)
if None in (close, bb_upper, bb_lower, rsi):
return {"action":"wait","reason":"Key indicator values are None."}
# ── H4 context ───────────────────────────────────────────────────
h4_sma = get_h4_context(symbol)
ctx_long = True
ctx_short = True
ctx_note = "H4 filter skipped"
if h4_sma is not None and REQUIRE_CTX:
ctx_long = close > h4_sma * 0.9995 # Allow slight dip below H4 SMA
ctx_short = close < h4_sma * 1.0005
ctx_note = f"H4 SMA{TREND_MA_PER}={h4_sma:.5f}"
# ── Signal detection (2 hard conditions + soft bonuses) ──────────
buy_hard = (close < bb_lower) and (rsi < RSI_OS)
sell_hard = (close > bb_upper) and (rsi > RSI_OB)
if not buy_hard and not sell_hard:
return {
"action": "wait",
"reason": (
f"No signal. Close={close:.5f} BB=[{bb_lower:.5f},{bb_upper:.5f}] "
f"RSI={rsi:.1f}"
)
}
direction = "Buy" if buy_hard else "Sell"
# ── Soft bonus conditions (don't block, but affect confidence) ────
if direction == "Buy":
conds = [
(close < bb_lower, f"Price closed below lower BB ({close:.5f} < {bb_lower:.5f})"),
(rsi < RSI_OS, f"RSI oversold ({rsi:.1f} < {RSI_OS})"),
(ctx_long, f"H4 context OK — {ctx_note}"),
(stoch_k is not None and stoch_k < 25, f"Stochastic K oversold ({stoch_k:.1f})"),
(obv_up, f"OBV rising (volume supports buy)"),
(adx is not None and adx < 30, f"ADX={adx:.1f} (ranging market — ideal for reversion)"),
(macd_hist is not None and macd_hist > -0.00005, f"MACD hist not strongly bearish"),
]
entry = ask
if atr and atr > 0:
sl = round(entry - atr * SL_ATR_MULT, 6)
tp = round(entry + atr * TP_ATR_MULT, 6)
else:
sl = round(entry - 15 * pip_size(symbol), 6)
tp = round(entry + 30 * pip_size(symbol), 6)
else:
conds = [
(close > bb_upper, f"Price closed above upper BB ({close:.5f} > {bb_upper:.5f})"),
(rsi > RSI_OB, f"RSI overbought ({rsi:.1f} > {RSI_OB})"),
(ctx_short, f"H4 context OK — {ctx_note}"),
(stoch_k is not None and stoch_k > 75, f"Stochastic K overbought ({stoch_k:.1f})"),
(not obv_up, f"OBV falling (volume supports sell)"),
(adx is not None and adx < 30, f"ADX={adx:.1f} (ranging market — ideal for reversion)"),
(macd_hist is not None and macd_hist < 0.00005, f"MACD hist not strongly bullish"),
]
entry = bid
if atr and atr > 0:
sl = round(entry + atr * SL_ATR_MULT, 6)
tp = round(entry - atr * TP_ATR_MULT, 6)
else:
sl = round(entry + 15 * pip_size(symbol), 6)
tp = round(entry - 30 * pip_size(symbol), 6)
passed = [(m, d) for m, d in conds if m]
failed = [(m, d) for m, d in conds if not m]
# Athena only requires 2 hard conditions (already confirmed above)
# Confidence based on how many soft bonuses also fired
n_passed = len(passed)
confidence = "high" if n_passed >= 5 else ("medium" if n_passed >= 3 else "low")
# ── R:R gate ─────────────────────────────────────────────────────
sl_pips = price_to_pips(entry - sl, symbol)
tp_pips = price_to_pips(tp - entry, symbol)
rr = round(tp_pips / sl_pips, 2) if sl_pips > 0 else 0
if rr < MIN_RR:
return {"action":"wait","reason":f"R:R {rr} below minimum {MIN_RR}."}
volume = calculate_lot(equity, sl_pips, symbol)
# Update rate-limit state
_last_signal_time[symbol] = time.time()
hour_key = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H")
_signals_this_hour[hour_key] = _signals_this_hour.get(hour_key, 0) + 1
reason = (
f"Athena BB+RSI M5: Close={'below' if direction=='Buy' else 'above'} "
f"{'lower' if direction=='Buy' else 'upper'} BB, RSI={rsi:.1f}. "
f"{n_passed}/7 conditions. ATR={atr:.5f}, R:R={rr}."
)
log.info(f"SIGNAL: {direction} {symbol} | SL={sl} TP={tp} Vol={volume} RR={rr}")
return {
"action": "trade",
"strategy": "athena-bb-rsi-m5",
"signal_type": "BB_LOWER_TOUCH" if direction=="Buy" else "BB_UPPER_TOUCH",
"symbol": symbol,
"direction": direction,
"entry": entry,
"stop_loss": sl,
"take_profit": tp,
"volume": volume,
"rr_ratio": rr,
"sl_pips": round(sl_pips, 1),
"tp_pips": round(tp_pips, 1),
"confidence": confidence,
"conditions_met": [d for _, d in passed],
"conditions_failed": [d for _, d in failed],
"warnings": news_warn,
"reason": reason,
"indicators": {
"close": close, "bb_upper": bb_upper, "bb_lower": bb_lower,
"bb_middle": ind.get("bb_middle"), "bb_width": ind.get("bb_width"),
"rsi": rsi, "stoch_k": stoch_k, "adx": adx,
"atr": atr, "h4_sma50": h4_sma, "spread_pips": spread_pips,
},
"signal_schema": { # Matches the SignalMessage spec from the prompt
"strategy_id": "ATHENA-v1",
"magic_number": CFG["strategy"]["magic_number"],
"risk_percent": RISK_PCT,
"metadata": {
"bb_lower": bb_lower, "bb_middle": ind.get("bb_middle"),
"bb_upper": bb_upper, "rsi": rsi,
}
},
"analysed_at": datetime.now(timezone.utc).isoformat(),
}
if __name__ == "__main__":
import sys
sym = sys.argv[1] if len(sys.argv) > 1 else "EURUSDxx"
print(f"Running Athena analysis for {sym}...")
result = run_analysis(sym)
print(json.dumps(result, indent=2, default=str))
+365
View File
@@ -0,0 +1,365 @@
#!/usr/bin/env python3
"""
GENESIS Athena Telegram Bot (Strategy D Command Handler)
Same pattern as ares/apollo bots. Hermes controls autonomously.
Commands:
/athena_analyze [SYMBOL] BB+RSI analysis on M5, no trade
/athena_scan Scan all symbols, queue best signal
/athena_execute Execute pending signal
/athena_skip Cancel pending signal
/athena_status Account + open Athena position + journal
/athena_help All commands
"""
import os, json, time, logging, threading, sys
from datetime import datetime, timezone
from pathlib import Path
import yaml, requests
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "core"))
from mt5_bridge import bridge as _bridge
CONFIG_PATH = Path(__file__).parent / "athena_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "athena_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(os.getenv("TELEGRAM_CHAT_ID", CFG["telegram"]["chat_id"]))
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "athena"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
STRATEGY = CFG["strategy"]["name"]
COMMENT = CFG["strategy"]["comment"]
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/athena/athena_bot.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "athena"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "athena_bot.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
_pending: dict = {}
_lock = threading.Lock()
def tg_send(text: str):
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "Markdown"},
timeout=10
)
except Exception as e:
log.error(f"tg_send: {e}")
def tg_updates(offset=0):
try:
r = requests.get(
f"https://api.telegram.org/bot{TG_TOKEN}/getUpdates",
params={"timeout": 30, "offset": offset}, timeout=40
)
return r.json().get("result", [])
except:
return []
def bridge_call(path, method="GET", data=None):
return _bridge(path, method, data)
def journal_stats():
wins = losses = 0
pnl = 0.0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t = json.loads(line)
if t.get("result") == "win": wins += 1
if t.get("result") == "loss": losses += 1
pnl += float(t.get("pnl") or 0)
except: pass
return wins, losses, round(pnl, 2)
def cmd_help():
bb = CFG["indicators"]["bb_period"]
dev = CFG["indicators"]["bb_deviation"]
rsi = CFG["indicators"]["rsi_period"]
tf = CFG["indicators"]["signal_timeframe"]
tg_send(
f"🌿 *{STRATEGY} — Strategy D Commands*\n\n"
f"`/athena_analyze [SYMBOL]` — BB+RSI analysis (no trade)\n"
f"`/athena_scan` — Scan all {len(CFG['symbols'])} symbols\n"
f"`/athena_execute` — Execute pending signal\n"
f"`/athena_skip` — Cancel pending signal\n"
f"`/athena_status` — Position + journal stats\n"
f"`/athena_help` — This message\n\n"
f"📊 Strategy: BB({bb},{dev})+RSI({rsi}) on {tf}\n"
f"🎯 Risk: {CFG['risk']['risk_pct']*100}% | ATR-based SL/TP\n"
f"🔖 Tag: `{COMMENT}`"
)
def cmd_status():
acc = bridge_call("/balance")
if "error" in acc:
tg_send(f"🔴 *{STRATEGY}*: Bridge unreachable."); return
positions = bridge_call("/positions")
pos_str = "None"
all_str = []
if isinstance(positions, list):
for p in positions:
c = str(p.get("comment",""))
all_str.append(f"`{p.get('symbol')}` {p.get('orderType')} "
f"{p.get('lots')}lot €{p.get('profit',0):.2f} [{c}]")
if "ATHENA" in c.upper():
pos_str = f"{p.get('symbol')} {p.get('orderType')} | €{p.get('profit',0):.2f}"
wins, losses, pnl = journal_stats()
with _lock:
pend = (f"🟡 {_pending.get('symbol')} {_pending.get('direction')} "
f"({_pending.get('signal_type','?')})"
if _pending else "None")
tg_send(
f"🌿 *{STRATEGY} — Status*\n\n"
f"💰 Balance: €{acc.get('balance',0):.2f} | Equity: €{acc.get('equity',0):.2f}\n"
f"📈 Athena Position: {pos_str}\n"
f"📋 Pending: {pend}\n"
f"📒 Journal: {wins}W / {losses}L | PnL: €{pnl}\n\n"
f"*All Open:*\n" + ("\n".join(all_str) if all_str else "None")
)
def cmd_skip():
with _lock:
if not _pending:
tg_send(f"🌿 *{STRATEGY}*: No pending signal."); return
sym = _pending.get("symbol")
_pending.clear()
tg_send(f"⏭ *{STRATEGY}*: Signal for `{sym}` cancelled.")
def cmd_execute():
with _lock:
if not _pending:
tg_send(f"🌿 *{STRATEGY}*: No pending signal.\nRun `/athena_analyze SYMBOL` or `/athena_scan` first.")
return
signal = dict(_pending)
_pending.clear()
sym = signal.get("symbol")
dire = signal.get("direction")
sl = signal.get("stop_loss")
tp = signal.get("take_profit")
vol = signal.get("volume", 0.01)
if not all([sym, dire, sl, tp]):
tg_send(f"🌿 *{STRATEGY}*: Incomplete signal — cannot execute."); return
positions = bridge_call("/positions")
if isinstance(positions, list) and any(
"ATHENA" in str(p.get("comment","")).upper() for p in positions
):
tg_send(f"⚠️ *{STRATEGY}*: Athena position already open."); return
tg_send(f"🌿 *{STRATEGY}*: Placing order…")
order = bridge_call("/market","POST",{
"symbol": sym,"volume": vol,"type": dire,
"stop_loss": sl,"take_profit": tp,"comment": COMMENT
})
ticket = order.get("ticket") or order.get("Ticket")
if ticket:
now = datetime.now(timezone.utc)
Path(JOURNAL).parent.mkdir(parents=True, exist_ok=True)
with open(JOURNAL,"a") as f:
f.write(json.dumps({
"ticket": str(ticket),"symbol": sym,"direction": dire,
"volume": vol,"sl": sl,"tp": tp,
"signal_type": signal.get("signal_type"),
"confidence": signal.get("confidence"),
"rr": signal.get("rr_ratio"),
"opened": now.isoformat(),"result": None,"pnl": None,
"strategy": "athena-bb-rsi-m5"
}) + "\n")
ind = signal.get("indicators",{})
tg_send(
f"✅ *{STRATEGY} TRADE PLACED*\n"
f"📊 `{sym}` {dire} | {signal.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{signal.get('entry')}` | SL: `{sl}` | TP: `{tp}`\n"
f"R:R: `{signal.get('rr_ratio')}` | Vol: `{vol}` "
f"| Confidence: {signal.get('confidence','?')}\n"
f"RSI: {ind.get('rsi','?')} | ATR: {ind.get('atr','?')}\n"
f"🔖 Ticket: `{ticket}`"
)
else:
err = order.get("message",str(order))
tg_send(f"❌ *{STRATEGY}*: Order FAILED — `{err}`")
def _run_analysis(sym):
try:
import importlib.util
spec = importlib.util.spec_from_file_location(
"athena_cycle", Path(__file__).parent / "athena_cycle.py"
)
mod = importlib.util.load_from_spec(spec)
spec.loader.exec_module(mod)
return mod.run_analysis(sym)
except Exception as e:
log.error(f"Analysis error: {e}", exc_info=True)
return {"action":"wait","reason":f"Error: {str(e)[:200]}"}
def cmd_analyze(symbol: str):
sym = symbol.upper().strip()
if not sym.endswith("XX"):
sym = sym + "xx"
tg_send(f"🌿 *{STRATEGY}*: Analysing `{sym}` on M5… (3060s)")
result = _run_analysis(sym)
_format_and_send(result)
def cmd_scan():
tg_send(f"🌿 *{STRATEGY}*: Scanning {len(CFG['symbols'])} symbols… (6090s)")
symbols = CFG["symbols"]
best = None
best_rr = 0
lines = []
for sym in symbols:
r = _run_analysis(sym)
action = r.get("action","wait")
if action == "trade":
rr = r.get("rr_ratio",0) or 0
conf = r.get("confidence","?")
lines.append(f"🟢 `{sym}`: {r.get('direction')} "
f"{r.get('signal_type','').replace('_',' ')} "
f"R:R {rr} | {conf}")
if rr > best_rr:
best_rr = rr
best = r
else:
lines.append(f"⚪ `{sym}`: {r.get('reason','')[:60]}")
time.sleep(0.5)
tg_send(f"🌿 *{STRATEGY} SCAN*\n\n" + "\n".join(lines))
if best:
with _lock:
_pending.clear()
_pending.update(best)
_format_and_send(best, from_scan=True)
else:
tg_send(f"📊 *{STRATEGY}*: No trade signals found.")
def _format_and_send(result: dict, from_scan: bool = False):
action = result.get("action","wait")
if action != "trade":
tg_send(
f"🌿 *{STRATEGY}* — `{result.get('symbol','?')}`\n\n"
f"Signal: *WAIT*\n💡 {result.get('reason','')[:300]}"
)
return
with _lock:
_pending.clear()
_pending.update(result)
ind = result.get("indicators",{})
conds = result.get("conditions_met",[])
cond_str = "\n".join(f"{c}" for c in conds) if conds else " BB + RSI conditions met"
scan_tag = " _(Best from scan)_" if from_scan else ""
tg_send(
f"🌿 *{STRATEGY} ANALYSIS*{scan_tag} — `{result.get('symbol')}`\n\n"
f"Signal: *{result.get('direction')}* — "
f"{result.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{result.get('entry')}`\n"
f"SL: `{result.get('stop_loss')}` ({result.get('sl_pips','?')} pips)\n"
f"TP: `{result.get('take_profit')}` ({result.get('tp_pips','?')} pips)\n"
f"R:R: `{result.get('rr_ratio')}` | Vol: `{result.get('volume')}`\n"
f"🎯 Confidence: {result.get('confidence','?')}\n"
f"RSI: {ind.get('rsi','?')} | ATR: {ind.get('atr','?')} "
f"| ADX: {ind.get('adx','?')}\n\n"
f"📌 *Conditions ({len(conds)} fired):*\n{cond_str}\n\n"
f"💡 {result.get('reason','')[:250]}\n\n"
f"Reply `/athena_execute` to trade or `/athena_skip` to cancel."
)
def dispatch(text: str, from_id: str):
if str(from_id) != TG_CHAT_ID:
return
lower = text.lower().strip()
if lower.startswith("/athena_analyze"):
parts = text.split(maxsplit=1)
sym = parts[1] if len(parts) > 1 else ""
if not sym:
tg_send("Usage: `/athena_analyze EURUSD`")
else:
threading.Thread(target=cmd_analyze, args=(sym,), daemon=True).start()
elif lower == "/athena_scan":
threading.Thread(target=cmd_scan, daemon=True).start()
elif lower == "/athena_execute":
threading.Thread(target=cmd_execute, daemon=True).start()
elif lower == "/athena_skip":
cmd_skip()
elif lower == "/athena_status":
threading.Thread(target=cmd_status, daemon=True).start()
elif lower in ("/athena_help", "/athena"):
cmd_help()
def main():
log.info(f"=== {STRATEGY} Telegram Bot started ===")
try:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/setMyCommands",
json={"commands": [
{"command": "athena_help", "description": "Show all commands"},
{"command": "athena_analyze", "description": "BB+RSI analysis on symbol"},
{"command": "athena_scan", "description": "Scan all symbols for best signal"},
{"command": "athena_execute", "description": "Execute pending signal"},
{"command": "athena_skip", "description": "Cancel pending signal"},
{"command": "athena_status", "description": "Position + journal stats"},
]},
timeout=10
)
except Exception as e:
log.warning(f"setMyCommands failed: {e}")
bb = CFG["indicators"]["bb_period"]
dev = CFG["indicators"]["bb_deviation"]
rsi = CFG["indicators"]["rsi_period"]
tf = CFG["indicators"]["signal_timeframe"]
tg_send(
f"🌿 *{STRATEGY} Bot Online*\n"
f"Strategy D: BB({bb},{dev})+RSI({rsi}) Mean Reversion on {tf}\n"
f"Send `/athena_help` to see commands.\n\n"
f"🤖 _Hermes controls this bot autonomously._\n"
f"_You can also trigger manually via the commands above._"
)
offset = 0
while True:
try:
updates = tg_updates(offset)
for upd in updates:
offset = upd["update_id"] + 1
msg = upd.get("message",{})
text = msg.get("text","")
chat_id = str(msg.get("chat",{}).get("id",""))
if text.startswith("/athena"):
dispatch(text, chat_id)
except Exception as e:
log.error(f"Polling error: {e}")
time.sleep(5)
time.sleep(1)
if __name__ == "__main__":
main()
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""
GENESIS Athena CLI Tool (Strategy D: BB+RSI Mean Reversion M5)
Hermes calls this autonomously same pattern as ares_tool.py / apollo_tool.py.
Usage:
python3 athena_tool.py analyze EURUSD # BB+RSI analysis, no trade
python3 athena_tool.py execute EURUSD # Analyze + execute if signal found
python3 athena_tool.py scan # Scan all symbols, return best signal
python3 athena_tool.py status # Open Athena position + journal stats
python3 athena_tool.py close # Close open Athena position
python3 athena_tool.py symbols # List Athena symbols
"""
import sys, os, json, time
sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent))
from athena_cycle import (
run_analysis, bridge, JOURNAL, MAGIC_COMMENT,
tg, CFG, _last_signal_time, pip_size
)
from datetime import datetime, timezone
from pathlib import Path
ATHENA_SYMBOLS = CFG["symbols"]
def journal_write(entry: dict):
JOURNAL.parent.mkdir(parents=True, exist_ok=True)
with open(JOURNAL, "a") as f:
f.write(json.dumps(entry) + "\n")
def journal_stats():
wins = losses = total_pnl = 0.0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t = json.loads(line)
if t.get("result") == "win": wins += 1
if t.get("result") == "loss": losses += 1
total_pnl += float(t.get("pnl") or 0)
except: pass
return int(wins), int(losses), round(total_pnl, 2)
def cmd_analyze(symbol: str) -> dict:
sym = symbol.upper()
if not sym.endswith("XX"):
sym = sym + "xx"
result = run_analysis(sym)
print(json.dumps(result, indent=2, default=str))
return result
def cmd_execute(symbol: str) -> dict:
sym = symbol.upper()
if not sym.endswith("XX"):
sym = sym + "xx"
result = run_analysis(sym)
if result.get("action") != "trade":
out = {
"executed": False,
"reason": result.get("reason","No signal"),
"confidence": result.get("confidence","none"),
"conditions_met": result.get("conditions_met",[]),
}
print(json.dumps(out, indent=2))
return out
order = bridge("/market", "POST", {
"symbol": result["symbol"],
"volume": result["volume"],
"type": result["direction"],
"stop_loss": result["stop_loss"],
"take_profit": result["take_profit"],
"comment": MAGIC_COMMENT, # "ATHENA-v1"
})
ticket = order.get("ticket") or order.get("Ticket")
now = datetime.now(timezone.utc)
if ticket:
journal_write({
"ticket": str(ticket),
"symbol": result["symbol"],
"direction": result["direction"],
"volume": result["volume"],
"sl": result["stop_loss"],
"tp": result["take_profit"],
"entry": result["entry"],
"rr": result["rr_ratio"],
"signal_type": result.get("signal_type"),
"confidence": result.get("confidence"),
"opened": now.isoformat(),
"result": None,
"pnl": None,
"strategy": "athena-bb-rsi-m5",
"triggered_by": "hermes-autonomous",
"conditions": result.get("conditions_met",[]),
})
ind = result.get("indicators",{})
bb_period = CFG["indicators"]["bb_period"]
rsi_period = CFG["indicators"]["rsi_period"]
tg(
f"🌿 *ATHENA TRADE — Hermes Triggered*\n"
f"📊 `{result['symbol']}` {result['direction']} "
f"| {result.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{result['entry']}` | SL: `{result['stop_loss']}` "
f"| TP: `{result['take_profit']}`\n"
f"R:R: `{result['rr_ratio']}` | Vol: `{result['volume']}` "
f"| Confidence: {result.get('confidence','?')}\n"
f"🎯 BB({bb_period},2.0)+RSI({rsi_period}) on M5\n"
f"RSI: {ind.get('rsi','?')} | ATR: {ind.get('atr','?')} "
f"| ADX: {ind.get('adx','?')}\n"
f"📌 {', '.join(result.get('conditions_met',[])[:3])}\n"
f"🔖 Ticket: `{ticket}`"
)
out = {
"executed": True,
"ticket": str(ticket),
"symbol": result["symbol"],
"direction": result["direction"],
"signal_type": result.get("signal_type"),
"volume": result["volume"],
"sl": result["stop_loss"],
"tp": result["take_profit"],
"rr": result["rr_ratio"],
"confidence": result.get("confidence"),
"reason": result["reason"],
}
else:
err = order.get("message", str(order))
tg(f"⚠️ *ATHENA*: Order FAILED — `{err}`")
out = {"executed":False,"reason":f"Order failed: {err}"}
print(json.dumps(out, indent=2, default=str))
return out
def cmd_scan() -> dict:
"""Scan ALL symbols, return the best BB+RSI signal. Hermes calls this first."""
best = None
best_score = 0
results = {}
for sym in ATHENA_SYMBOLS:
r = run_analysis(sym)
action = r.get("action","wait")
results[sym] = {
"action": action,
"signal_type": r.get("signal_type","none"),
"confidence": r.get("confidence","none"),
"rr": r.get("rr_ratio"),
"reason": r.get("reason","")[:100],
}
if action == "trade":
conf_score = {"high":3,"medium":2,"low":1}.get(r.get("confidence","low"),1)
score = conf_score + (r.get("rr_ratio") or 0) * 0.5
score += len(r.get("conditions_met",[])) * 0.2
if score > best_score:
best_score = score
best = r
time.sleep(0.5)
out = {
"best_signal": best,
"scan_results": results,
"scanned": len(ATHENA_SYMBOLS),
"signals_found": sum(1 for r in results.values() if r["action"]=="trade"),
}
print(json.dumps(out, indent=2, default=str))
return out
def cmd_status() -> dict:
acc = bridge("/balance")
positions = bridge("/positions")
athena_pos = None
all_pos_str = []
if isinstance(positions, list):
for p in positions:
c = str(p.get("comment",""))
all_pos_str.append({
"ticket": p.get("ticket"),
"symbol": p.get("symbol"),
"type": p.get("orderType"),
"lots": p.get("lots"),
"profit": p.get("profit"),
"comment": c,
})
if "ATHENA" in c.upper():
athena_pos = p
wins, losses, pnl = journal_stats()
out = {
"account": acc,
"athena_position": athena_pos,
"all_open": all_pos_str,
"athena_journal": {"wins": wins, "losses": losses, "total_pnl": pnl},
"strategy": "BB+RSI Mean Reversion M5",
"comment_tag": MAGIC_COMMENT,
}
print(json.dumps(out, indent=2, default=str))
return out
def cmd_close() -> dict:
positions = bridge("/positions")
closed = []
if isinstance(positions, list):
for p in positions:
if "ATHENA" in str(p.get("comment","")).upper():
res = bridge("/close","POST",{"ticket": p["ticket"]})
closed.append({"ticket": p["ticket"], "result": res})
tg(f"🌿 *ATHENA*: Position `{p['ticket']}` closed by Hermes.")
out = ({"closed": len(closed),"positions": closed}
if closed else {"closed":0,"reason":"No open Athena positions."})
print(json.dumps(out, indent=2, default=str))
return out
def cmd_symbols() -> dict:
bb = CFG["indicators"]["bb_period"]
dev = CFG["indicators"]["bb_deviation"]
rsi = CFG["indicators"]["rsi_period"]
out = {
"symbols": ATHENA_SYMBOLS,
"strategy": "BB+RSI Mean Reversion",
"timeframe": f"{CFG['indicators']['signal_timeframe']} entry, "
f"{CFG['indicators']['trend_timeframe']} context",
"indicators": f"BB({bb},{dev}) + RSI({rsi})",
"comment_tag": MAGIC_COMMENT,
"risk": f"{CFG['risk']['risk_pct']*100}% per trade",
}
print(json.dumps(out, indent=2))
return out
# ── Entry point ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
args = sys.argv[1:]
if not args:
print(json.dumps({"error":"Usage: athena_tool.py [analyze|execute|scan|status|close|symbols] [SYMBOL]"}))
sys.exit(1)
cmd = args[0].lower()
if cmd == "analyze":
if len(args)<2: print(json.dumps({"error":"analyze requires a symbol"})); sys.exit(1)
cmd_analyze(args[1])
elif cmd == "execute":
if len(args)<2: print(json.dumps({"error":"execute requires a symbol"})); sys.exit(1)
cmd_execute(args[1])
elif cmd == "scan":
cmd_scan()
elif cmd == "status":
cmd_status()
elif cmd == "close":
cmd_close()
elif cmd == "symbols":
cmd_symbols()
else:
print(json.dumps({"error":f"Unknown: {cmd}"})); sys.exit(1)
+443
View File
@@ -0,0 +1,443 @@
#!/usr/bin/env python3
"""
GENESIS Hephaestus (Strategy F: Grid + Martingale)
EXTREME RISK WARNING
Grid/Martingale strategies can produce 90%+ win rates but carry the risk of
CATASTROPHIC, UNLIMITED DRAWDOWN if price trends strongly without reversal.
Circuit breakers in this code REDUCE but DO NOT ELIMINATE this risk.
NEVER run on live account without:
- Backtesting over trending AND ranging regimes
- max_grid_levels 5 and initial_lot = 0.01
- Monitoring at least daily
- Setting confirm_risk_acknowledged: true only after understanding the above
Architecture: This runs as a STATE MACHINE, not a signal generator.
- State is persisted in /var/log/hephaestus/grid_state.json
- Hermes calls hephaestus_tool.py to check status / start / stop
- The tool is NOT meant to run continuously it's polled by Hermes
"""
import os, json, time, logging, math
from datetime import datetime, timezone, date
from pathlib import Path
import requests, yaml
CONFIG_PATH = Path(__file__).parent / "hephaestus_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "hephaestus_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
# ── Risk gate — hard stop if user hasn't acknowledged ─────────────────────────
if not CFG["strategy"].get("confirm_risk_acknowledged"):
raise RuntimeError(
"HEPHAESTUS BLOCKED: Set confirm_risk_acknowledged: true in hephaestus_config.yaml "
"after reading the risk warning. This strategy can blow your account."
)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(CFG["telegram"]["chat_id"])
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "hephaestus"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
STATE_FILE = Path("/var/log/hephaestus/grid_state.json")
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
COMMENT = CFG["strategy"]["comment"]
SYMBOL = CFG["symbol"]
DIRECTION = CFG["direction"]
INIT_LOT = float(CFG["grid"]["initial_lot"])
MULTIPLIER = float(CFG["grid"]["martingale_multiplier"])
MAX_LOT = float(CFG["grid"]["max_lot_per_order"])
SPACING = int(CFG["grid"]["grid_spacing_pips"])
MAX_LEVELS = int(CFG["grid"]["max_grid_levels"])
TP_PIPS = int(CFG["grid"]["take_profit_pips"])
BASKET_TP = int(CFG["grid"]["basket_tp_pips"])
MAX_DD_PCT = float(CFG["circuit_breakers"]["max_equity_drawdown_pct"])
MAX_DL_PCT = float(CFG["circuit_breakers"]["max_daily_loss_pct"])
MAX_CONSEC = int(CFG["circuit_breakers"]["max_consecutive_losses"])
COOLDOWN = int(CFG["circuit_breakers"]["cooldown_after_reset_sec"])
MAX_SPREAD = float(CFG["circuit_breakers"]["max_spread_pips"])
MAX_LOTS = float(CFG["circuit_breakers"]["max_total_lots"])
START_H = int(CFG["sessions"]["allowed"][0]["start"])
END_H = int(CFG["sessions"]["allowed"][0]["end"])
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/hephaestus/hephaestus_cycle.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "hephaestus"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "hephaestus_cycle.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
# ── State management ───────────────────────────────────────────────────────────
DEFAULT_STATE = {
"enabled": True,
"buy_level": 0, # Current martingale level for buys (0=initial)
"sell_level": 0,
"buy_tickets": [], # Open buy position tickets
"sell_tickets": [], # Open sell position tickets
"consec_buy_loss": 0,
"consec_sell_loss": 0,
"peak_equity": 0.0,
"day_start_bal": 0.0,
"last_day": str(date.today()),
"last_reset_ts": 0,
"total_cycles": 0,
"killed_reason": None,
}
def load_state() -> dict:
if STATE_FILE.exists():
try: return json.loads(STATE_FILE.read_text())
except: pass
return dict(DEFAULT_STATE)
def save_state(s: dict):
STATE_FILE.write_text(json.dumps(s, default=str))
# ── Mt5Bridge (unified adapter) ────────────────────────────────────────────────
import sys
sys.path.insert(0, str(Path(__file__).parents[2] / "core"))
from mt5_bridge import bridge, get_bars as _bridge_get_bars, pip_size, calc_lot
def tg(msg: str):
try:
requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": msg, "parse_mode": "Markdown"}, timeout=10)
except: pass
def pip(sym): return 0.01 if "JPY" in sym else (0.1 if "XAU" in sym else 0.0001)
def to_pips(diff, sym): return abs(diff) / pip(sym)
def is_trade_time():
now = datetime.now(timezone.utc)
wd, hr = now.weekday(), now.hour
if (wd==4 and hr>=22) or wd==5 or (wd==6 and hr<22): return False
return START_H <= hr < END_H
def lot_for_level(level: int) -> float:
"""Martingale lot: initial × multiplier^level, capped at MAX_LOT."""
lot = INIT_LOT * (MULTIPLIER ** level)
return round(min(lot, MAX_LOT), 2)
def total_exposure(positions: list) -> float:
return sum(float(p.get("lots",0)) for p in positions
if COMMENT.split("-")[0] in str(p.get("comment","")))
def get_heph_positions(positions: list, direction: str = None) -> list:
tag = COMMENT.split("-")[0]
res = [p for p in (positions if isinstance(positions,list) else [])
if tag in str(p.get("comment","")).upper()]
if direction:
res = [p for p in res if p.get("orderType","").lower()==direction.lower()]
return res
# ── Circuit breaker evaluation ─────────────────────────────────────────────────
def check_circuit_breakers(state: dict, acc: dict) -> tuple[bool, str]:
"""Returns (killed, reason). Updates state in-place if kill triggered."""
equity = float(acc.get("equity", 0))
balance = float(acc.get("balance", 0))
# Reset daily tracking if new day
today = str(date.today())
if state["last_day"] != today:
state["last_day"] = today
state["day_start_bal"] = balance
log.info("New day — daily loss counter reset.")
if state["peak_equity"] < equity:
state["peak_equity"] = equity
# 1. Equity drawdown from peak
if state["peak_equity"] > 0:
dd_pct = (state["peak_equity"] - equity) / state["peak_equity"] * 100
if dd_pct >= MAX_DD_PCT:
return True, f"EQUITY DRAWDOWN {dd_pct:.2f}% ≥ {MAX_DD_PCT}% — EMERGENCY STOP"
# 2. Daily loss
if state["day_start_bal"] > 0:
daily_loss_pct = (state["day_start_bal"] - balance) / state["day_start_bal"] * 100
if daily_loss_pct >= MAX_DL_PCT:
return True, f"DAILY LOSS {daily_loss_pct:.2f}% ≥ {MAX_DL_PCT}% — STOPPED FOR DAY"
# 3. Consecutive losses
if state["consec_buy_loss"] >= MAX_CONSEC or state["consec_sell_loss"] >= MAX_CONSEC:
return True, f"MAX CONSECUTIVE LOSSES ({MAX_CONSEC}) reached — RESET GRID"
return False, ""
def emergency_stop(state: dict, reason: str, positions: list) -> dict:
"""Close ALL Hephaestus positions and disable strategy."""
log.error(f"EMERGENCY STOP: {reason}")
tg(f"🚨 *HEPHAESTUS EMERGENCY STOP*\n`{reason}`\nClosing all grid positions now.")
closed = 0
for p in get_heph_positions(positions):
r = bridge("/close","POST",{"ticket": p["ticket"]})
if r.get("ticket") or not r.get("error"): closed += 1
state["enabled"] = False
state["killed_reason"] = reason
state["buy_level"] = 0
state["sell_level"] = 0
state["buy_tickets"] = []
state["sell_tickets"] = []
save_state(state)
tg(f"🚨 *HEPHAESTUS*: {closed} positions closed. Strategy DISABLED.")
return state
def reset_grid(state: dict, positions: list, reason: str = "basket TP hit") -> dict:
"""Close all positions, reset levels, apply cooldown."""
log.info(f"Grid reset: {reason}")
closed = 0
total_pnl = 0.0
for p in get_heph_positions(positions):
pnl = float(p.get("profit",0))
r = bridge("/close","POST",{"ticket": p["ticket"]})
if not r.get("error"):
closed += 1
total_pnl += pnl
_journal(p, pnl, "reset")
state.update({
"buy_level":0,"sell_level":0,
"buy_tickets":[],"sell_tickets":[],
"consec_buy_loss":0,"consec_sell_loss":0,
"last_reset_ts": time.time(),
"total_cycles": state.get("total_cycles",0) + 1,
})
save_state(state)
tg(f"🔄 *HEPHAESTUS GRID RESET* ({reason})\n"
f"Closed {closed} positions | Cycle PnL: €{total_pnl:.2f}\n"
f"Total cycles: {state['total_cycles']} | Cooldown: {COOLDOWN}s")
return state
def _journal(p, pnl, result):
with open(JOURNAL,"a") as f:
f.write(json.dumps({
"ticket":str(p.get("ticket")),"symbol":p.get("symbol"),
"type":p.get("orderType"),"lots":p.get("lots"),
"pnl":round(pnl,2),"result":result,
"ts":datetime.now(timezone.utc).isoformat(),"strategy":"hephaestus-grid"
})+"\n")
# ── Core cycle tick ────────────────────────────────────────────────────────────
def run_cycle() -> dict:
"""
Main Hephaestus logic tick. Called by hephaestus_tool.py on schedule.
Returns status dict describing current grid state and any actions taken.
"""
state = load_state()
actions = []
if not state["enabled"]:
return {"status":"disabled","reason":state.get("killed_reason","unknown"),"state":state}
# ── Account ─────────────────────────────────────────────────────
acc = bridge("/balance")
if "error" in acc:
return {"status":"error","reason":f"Bridge: {acc['error']}"}
equity = float(acc.get("equity",0))
balance = float(acc.get("balance",0))
# Initialize peak/day_start
if state["peak_equity"] == 0: state["peak_equity"] = equity
if state["day_start_bal"] == 0: state["day_start_bal"] = balance
# ── Circuit breakers ─────────────────────────────────────────────
positions = bridge("/positions")
if not isinstance(positions,list): positions = []
killed, kill_reason = check_circuit_breakers(state, acc)
if killed:
state = emergency_stop(state, kill_reason, positions)
return {"status":"emergency_stop","reason":kill_reason}
# ── Session check ────────────────────────────────────────────────
if not is_trade_time():
save_state(state)
return {"status":"outside_session","equity":equity,"state":state}
# ── Cooldown check ───────────────────────────────────────────────
if time.time() - state["last_reset_ts"] < COOLDOWN:
remaining = int(COOLDOWN - (time.time()-state["last_reset_ts"]))
save_state(state)
return {"status":"cooldown","remaining_seconds":remaining}
# ── Quote + spread ───────────────────────────────────────────────
quote = bridge(f"/quote?symbol={SYMBOL}")
if "error" in quote or not quote.get("bid"):
return {"status":"no_quote"}
bid = float(quote["bid"]); ask = float(quote["ask"])
spread_pips = to_pips(ask-bid, SYMBOL)
if spread_pips > MAX_SPREAD:
return {"status":"spread_too_wide","spread":spread_pips}
# ── Exposure check ───────────────────────────────────────────────
heph_pos = get_heph_positions(positions)
total_lots = total_exposure(positions)
if total_lots >= MAX_LOTS:
tg(f"⚠️ *HEPHAESTUS*: Max exposure {total_lots:.2f}lots ≥ {MAX_LOTS}. No new levels.")
save_state(state)
return {"status":"max_exposure","lots":total_lots}
# ── Check basket TP ──────────────────────────────────────────────
basket_pnl = sum(float(p.get("profit",0)) for p in heph_pos)
basket_tp_eur = BASKET_TP * pip(SYMBOL) * 100000 * INIT_LOT # Approx EUR value
if heph_pos and basket_pnl >= basket_tp_eur:
state = reset_grid(state, heph_pos, f"basket TP hit €{basket_pnl:.2f}")
save_state(state)
return {"status":"basket_tp_hit","pnl":basket_pnl}
# ── Check individual position outcomes ───────────────────────────
for p in heph_pos:
ticket = str(p.get("ticket"))
pnl = float(p.get("profit",0))
tp_eur = TP_PIPS * pip(SYMBOL) * 100000 * float(p.get("lots",0.01))
# Close if individual TP hit
if pnl >= tp_eur:
r = bridge("/close","POST",{"ticket": p["ticket"]})
if not r.get("error"):
_journal(p, pnl, "win")
dir_ = p.get("orderType","Buy")
if dir_ == "Buy":
state["consec_buy_loss"] = 0
if ticket in [str(t) for t in state["buy_tickets"]]:
state["buy_tickets"] = [t for t in state["buy_tickets"] if str(t)!=ticket]
if state["buy_level"] > 0: state["buy_level"] -= 1
else:
state["consec_sell_loss"] = 0
if ticket in [str(t) for t in state["sell_tickets"]]:
state["sell_tickets"] = [t for t in state["sell_tickets"] if str(t)!=ticket]
if state["sell_level"] > 0: state["sell_level"] -= 1
actions.append(f"Closed TP {dir_} ticket {ticket} P&L €{pnl:.2f}")
log.info(f"TP hit: {dir_} ticket {ticket} PnL={pnl:.2f}")
# ── Open new grid level if no position in that direction ──────────
def open_level(direction: str):
level = state[f"{direction.lower()}_level"]
if level >= MAX_LEVELS:
tg(f"⚠️ *HEPHAESTUS*: Max levels ({MAX_LEVELS}) reached for {direction}. Waiting.")
return None
lot = lot_for_level(level)
sl_price = (round(bid - SPACING*2*pip(SYMBOL),6) if direction=="Buy"
else round(ask + SPACING*2*pip(SYMBOL),6))
tp_price = (round(ask + TP_PIPS*pip(SYMBOL),6) if direction=="Buy"
else round(bid - TP_PIPS*pip(SYMBOL),6))
order = bridge("/market","POST",{
"symbol":SYMBOL,"volume":lot,"type":direction,
"stop_loss":sl_price,"take_profit":tp_price,"comment":COMMENT
})
ticket = order.get("ticket") or order.get("Ticket")
if ticket:
state[f"{direction.lower()}_tickets"].append(str(ticket))
state[f"{direction.lower()}_level"] = level + 1
_journal({"ticket":ticket,"symbol":SYMBOL,"orderType":direction,"lots":lot,"profit":0}, 0, "open")
log.info(f"Grid {direction} Level {level} opened: lot={lot} ticket={ticket}")
actions.append(f"Opened {direction} Level {level} lot={lot} ticket={ticket}")
tg(f"🔩 *HEPHAESTUS*: {direction} Level {level+1} | Lot {lot} | Ticket `{ticket}`")
return ticket
else:
log.error(f"Order failed: {order}")
return None
# Open buys if no active buy position
active_buys = get_heph_positions(heph_pos, "Buy")
active_sells = get_heph_positions(heph_pos, "Sell")
if DIRECTION in ("buy_only","both") and not active_buys:
open_level("Buy")
if DIRECTION in ("sell_only","both") and not active_sells:
open_level("Sell")
save_state(state)
return {
"status": "running",
"equity": equity,
"basket_pnl": round(basket_pnl,2),
"total_lots": round(total_lots,2),
"buy_level": state["buy_level"],
"sell_level": state["sell_level"],
"spread": round(spread_pips,2),
"actions": actions,
"open_positions": len(heph_pos),
"state": {k:v for k,v in state.items() if k not in ("buy_tickets","sell_tickets")},
}
def get_status() -> dict:
"""Status snapshot — does NOT modify state or open orders."""
state = load_state()
acc = bridge("/balance")
pos = bridge("/positions")
heph = get_heph_positions(pos if isinstance(pos,list) else [])
pnl = sum(float(p.get("profit",0)) for p in heph)
lots = sum(float(p.get("lots",0)) for p in heph)
w=l=0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t=json.loads(line)
if t.get("result")=="win": w+=1
if t.get("result")=="loss": l+=1
except: pass
return {
"enabled": state["enabled"],
"killed_reason": state.get("killed_reason"),
"buy_level": state["buy_level"],
"sell_level": state["sell_level"],
"open_positions": len(heph),
"total_lots": round(lots,2),
"unrealized_pnl": round(pnl,2),
"peak_equity": state["peak_equity"],
"total_cycles": state["total_cycles"],
"account": acc,
"journal": {"wins":w,"losses":l},
"circuit_breakers": {
"max_dd_pct": MAX_DD_PCT,
"max_daily_loss":MAX_DL_PCT,
"max_levels": MAX_LEVELS,
"max_lots": MAX_LOTS,
}
}
def emergency_kill() -> dict:
"""Force-kill from external call (Hermes or manual)."""
state = load_state()
pos = bridge("/positions")
state = emergency_stop(state, "Manual kill via hephaestus_tool.py kill",
pos if isinstance(pos,list) else [])
return {"killed":True,"state":state}
def enable_strategy() -> dict:
state = load_state()
state["enabled"] = True
state["killed_reason"] = None
state["last_reset_ts"] = 0
save_state(state)
tg(f"✅ *HEPHAESTUS*: Strategy RE-ENABLED by Hermes.")
return {"enabled":True}
if __name__ == "__main__":
import sys
print(json.dumps(run_cycle(), indent=2, default=str))
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""GENESIS — Hephaestus Tool + Telegram Bot (Strategy F: Grid+Martingale)
CLI for Hermes. Bot for manual monitoring and override.
"""
import sys, os, json, time, logging, threading
sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent))
from pathlib import Path
from datetime import datetime, timezone
import yaml, requests
CONFIG_PATH = Path(__file__).parent / "hephaestus_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "hephaestus_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(CFG["telegram"]["chat_id"])
STRATEGY = CFG["strategy"]["name"]
COMMENT = CFG["strategy"]["comment"]
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/hephaestus/hephaestus_bot.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "hephaestus"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "hephaestus_bot.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
_lock = threading.Lock()
def tg_send(text):
try:
requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT_ID, "text": text, "parse_mode": "Markdown"}, timeout=10)
except: pass
def tg_updates(offset=0):
try:
r = requests.get(f"https://api.telegram.org/bot{TG_TOKEN}/getUpdates",
params={"timeout":30,"offset":offset}, timeout=40)
return r.json().get("result",[])
except: return []
def _load():
import importlib.util
spec = importlib.util.spec_from_file_location("hephaestus_cycle",
Path(__file__).parent/"hephaestus_cycle.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# ── Commands ───────────────────────────────────────────────────────────────────
def do_run_tick():
try: return _load().run_cycle()
except Exception as e: return {"error":str(e)}
def do_status():
try: return _load().get_status()
except Exception as e: return {"error":str(e)}
def do_kill():
try: return _load().emergency_kill()
except Exception as e: return {"error":str(e)}
def do_enable():
try: return _load().enable_strategy()
except Exception as e: return {"error":str(e)}
def send_status_tg():
r = do_status()
if "error" in r:
tg_send(f"🔴 *{STRATEGY}*: {r['error']}"); return
enabled = r.get("enabled")
acc = r.get("account",{})
j = r.get("journal",{})
cb = r.get("circuit_breakers",{})
killed = r.get("killed_reason")
tg_send(
f"{'🔩' if enabled else '💀'} *{STRATEGY} — Grid Status*\n\n"
f"Status: {'🟢 RUNNING' if enabled else f'🔴 DISABLED — {killed}'}\n"
f"💰 Balance: €{acc.get('balance',0):.2f} | Equity: €{acc.get('equity',0):.2f}\n"
f"📊 Open Positions: {r.get('open_positions',0)} | "
f"Lots: {r.get('total_lots',0)} | PnL: €{r.get('unrealized_pnl',0):.2f}\n"
f"📈 Buy Level: {r.get('buy_level',0)}/{cb.get('max_levels','?')} | "
f"Sell Level: {r.get('sell_level',0)}/{cb.get('max_levels','?')}\n"
f"🎯 Cycles: {r.get('total_cycles',0)} | "
f"Journal: {j.get('wins',0)}W / {j.get('losses',0)}L\n\n"
f"🛡 Circuit Breakers:\n"
f" Max DD: {cb.get('max_dd_pct','?')}% | Daily Loss: {cb.get('max_daily_loss','?')}%\n"
f" Max Levels: {cb.get('max_levels','?')} | Max Lots: {cb.get('max_lots','?')}"
)
def dispatch(text, from_id):
if str(from_id) != TG_CHAT_ID: return
lower = text.lower().strip()
def bg(fn, *args): threading.Thread(target=fn, args=args, daemon=True).start()
if lower == "/heph_status":
bg(send_status_tg)
elif lower == "/heph_tick":
# BLOCKED — only Hermes can run ticks via CLI
tg_send(
f"🔒 *{STRATEGY}*: `/heph_tick` is reserved for Hermes only.\n"
f"Hermes runs grid ticks autonomously via CLI.\n"
f"_You will see every action here automatically._"
)
elif lower == "/heph_kill":
def run():
tg_send(f"🚨 *{STRATEGY}*: EMERGENCY KILL initiated…")
r = do_kill()
tg_send(f"🚨 *{STRATEGY}*: {'All positions closed. DISABLED.' if r.get('killed') else 'Kill failed — check logs.'}")
bg(run)
elif lower == "/heph_enable":
def run():
r = do_enable()
tg_send(f"✅ *{STRATEGY}*: {'RE-ENABLED. Hermes will resume grid ticks.' if r.get('enabled') else 'Enable failed.'}")
bg(run)
elif lower in ("/heph_help", "/heph"):
tg_send(
f"🔩 *{STRATEGY} — Strategy F (Monitor & Override)*\n\n"
f"🤖 *Hermes runs this strategy autonomously.*\n"
f"_You will receive automatic updates for every grid action._\n\n"
f"`/heph_status` — Full grid status + circuit breaker state\n"
f"`/heph_kill` — 🚨 EMERGENCY: close all positions + disable\n"
f"`/heph_enable` — Re-enable after kill\n"
f"`/heph_help` — This message\n\n"
f"⚠️ Max {CFG['grid']['max_grid_levels']} levels | "
f"{CFG['circuit_breakers']['max_equity_drawdown_pct']}% equity DD kill\n"
f"🔖 Tag: `{COMMENT}` | Pair: `{CFG['symbol']}`"
)
# ── CLI mode (Hermes calls this) ───────────────────────────────────────────────
def cli():
args = sys.argv[1:]
if not args:
print(json.dumps({"error":"Usage: hephaestus_tool.py [tick|status|kill|enable|symbols]"}))
sys.exit(1)
cmd = args[0].lower()
if cmd=="tick": print(json.dumps(do_run_tick(), indent=2, default=str))
elif cmd=="status": print(json.dumps(do_status(), indent=2, default=str))
elif cmd=="kill": print(json.dumps(do_kill(), indent=2, default=str))
elif cmd=="enable": print(json.dumps(do_enable(), indent=2, default=str))
elif cmd=="symbols": print(json.dumps({"symbol":CFG["symbol"],"comment":COMMENT,"strategy":"Grid+Martingale"},indent=2))
else: print(json.dumps({"error":f"Unknown: {cmd}"}))
# ── Bot mode ───────────────────────────────────────────────────────────────────
def bot():
log.info(f"=== {STRATEGY} Telegram Bot started ===")
tg_send(
f"🔩 *{STRATEGY} Bot Online*\n"
f"Strategy F: Grid + Martingale on `{CFG['symbol']}`\n"
f"Max Levels: {CFG['grid']['max_grid_levels']} | "
f"DD Kill: {CFG['circuit_breakers']['max_equity_drawdown_pct']}%\n\n"
f"🤖 *Hermes runs this strategy. You cannot trigger it.*\n"
f"_You will see every grid action, level open, and TP here automatically._\n\n"
f"✅ Your controls: `/heph_status` `/heph_kill` `/heph_enable`\n"
f"⚠️ Use `/heph_kill` anytime to emergency stop all grid positions."
)
offset = 0
while True:
try:
for upd in tg_updates(offset):
offset = upd["update_id"]+1
msg = upd.get("message",{})
text = msg.get("text","")
chat_id = str(msg.get("chat",{}).get("id",""))
if text.startswith("/heph"): dispatch(text, chat_id)
except Exception as e:
log.error(f"Poll error: {e}"); time.sleep(5)
time.sleep(1)
if __name__ == "__main__":
if Path(sys.argv[0]).name.startswith("hephaestus_telegram"):
bot()
else:
cli()
+543
View File
@@ -0,0 +1,543 @@
#!/usr/bin/env python3
"""
GENESIS Zeus Cycle (Strategy G: ICT Smart Money Concepts)
Three-layer sequential confirmation NOT parallel detection:
Layer 1: Liquidity Sweep (price takes out swing high/low with rejection)
Layer 2: Fair Value Gap (3-candle imbalance after displacement)
Layer 3: Order Block (last candle before displacement, institutional anchor)
Only when ALL THREE confirm in sequence confluence score signal.
Expected: 5-15 signals/month. Win rate target: 65-70%.
"""
import os, json, time, logging, math
from datetime import datetime, timezone, date
from pathlib import Path
import requests, yaml
CONFIG_PATH = Path(__file__).parent / "zeus_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "zeus_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f:
CFG = yaml.safe_load(f)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(os.getenv("TELEGRAM_CHAT_ID", CFG["telegram"]["chat_id"]))
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "zeus"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
COMMENT = CFG["strategy"]["comment"]
# ICT config
SWING_LB = int(CFG["ict"]["liquidity"]["swing_lookback"])
SWEEP_TOL = float(CFG["ict"]["liquidity"]["sweep_tolerance"])
REQ_REJECT = bool(CFG["ict"]["liquidity"]["require_rejection"])
MIN_GAP_P = float(CFG["ict"]["fvg"]["min_gap_pips"])
FVG_AGE = int(CFG["ict"]["fvg"]["max_age_bars"])
OB_AGE = int(CFG["ict"]["order_block"]["max_age_bars"])
MIN_BODY = float(CFG["ict"]["order_block"]["min_body_ratio"])
MAX_WICK = float(CFG["ict"]["order_block"]["max_wick_ratio"])
MIN_SCORE = int(CFG["confluence"]["min_score"])
MAX_DT = int(CFG["confluence"]["max_daily_trades"])
KZ_EN = bool(CFG["confluence"]["killzone"]["enabled"])
KZ_LON = CFG["confluence"]["killzone"]["london"]
KZ_NY = CFG["confluence"]["killzone"]["ny"]
RISK_PCT = float(CFG["risk"]["risk_pct"])
MIN_RR = float(CFG["risk"]["min_rr"])
TP_MULT = float(CFG["risk"]["tp_multiplier"])
MAX_SPREAD = float(CFG["risk"]["max_spread_pips"])
COOLDOWN = int(CFG["risk"]["cooldown_seconds"])
OB_BUF = float(CFG["risk"]["sl_ob_buffer"])
MAX_DD_PCT = float(CFG["circuit_breakers"]["max_equity_drawdown_pct"])
MAX_DL_PCT = float(CFG["circuit_breakers"]["max_daily_loss_pct"])
START_H = int(CFG["sessions"]["allowed"][0]["start"])
END_H = int(CFG["sessions"]["allowed"][0]["end"])
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/zeus/zeus_cycle.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "zeus"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "zeus_cycle.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
_last_sig: dict = {}
_daily: dict = {}
# ── Mt5Bridge (unified adapter) ────────────────────────────────────────────────
import sys
sys.path.insert(0, str(Path(__file__).parents[2] / "core"))
from mt5_bridge import bridge, get_bars as _bridge_get_bars, pip_size, calc_lot
def tg(msg):
try:
requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id":TG_CHAT_ID,"text":msg,"parse_mode":"Markdown"}, timeout=10)
except: pass
def pip(sym): return 0.01 if "JPY" in sym else (0.1 if "XAU" in sym else 0.0001)
def to_pips(d,s): return abs(d)/pip(s)
def calc_lot(equity,sl_pips,sym):
pv=10.0
if "JPY" in sym: pv=9.0
if "GBP" in sym: pv=12.5
if "XAU" in sym: pv=1.0
raw=(equity*RISK_PCT)/(sl_pips*pv) if sl_pips>0 else 0.01
return round(max(0.01,min(round(raw/0.01)*0.01,5.0)),2)
YF_MAP={"EURUSDxx":"EURUSD=X","GBPUSDxx":"GBPUSD=X","USDJPYxx":"USDJPY=X",
"XAUUSDxx":"GC=F","GBPJPYxx":"GBPJPY=X",
"EURUSD":"EURUSD=X","GBPUSD":"GBPUSD=X","USDJPY":"USDJPY=X",
"XAUUSD":"GC=F","GBPJPY":"GBPJPY=X"}
def get_bars(sym,tf="M5",count=100):
try:
bars = _bridge_get_bars(sym, tf, count)
if bars:
return bars
except Exception as e:
log.warning(f"Mt5Bridge get_bars {sym}/{tf}: {e}, falling back to yfinance")
try:
import yfinance as yf, pandas as pd
yf_sym=YF_MAP.get(sym,sym.replace("xx","=X") if sym.lower().endswith("xx") else sym+"=X")
itv={"M5":"5m","M15":"15m","H1":"1h"}.get(tf,"5m")
per={"5m":"5d","15m":"5d","1h":"60d"}.get(itv,"5d")
df=yf.download(yf_sym,period=per,interval=itv,progress=False,auto_adjust=True)
if df.empty: return []
if isinstance(df.columns,pd.MultiIndex): df.columns=df.columns.get_level_values(0)
df.columns=[c.lower() for c in df.columns]
return df.dropna().tail(count).reset_index().to_dict("records")
except Exception as e:
log.error(f"get_bars {sym}/{tf}: {e}"); return []
# ── Layer 1: Liquidity Sweep Detection ────────────────────────────────────────
def detect_swing_highs(highs: list, lookback: int) -> list:
"""Pivot high: bar[i] is highest in [i-lb, i+lb] window."""
pivots = []
for i in range(lookback, len(highs)-lookback):
if highs[i] == max(highs[i-lookback:i+lookback+1]):
pivots.append((i, highs[i]))
return pivots
def detect_swing_lows(lows: list, lookback: int) -> list:
pivots = []
for i in range(lookback, len(lows)-lookback):
if lows[i] == min(lows[i-lookback:i+lookback+1]):
pivots.append((i, lows[i]))
return pivots
def detect_liquidity_sweep(highs, lows, closes, opens, sym) -> dict | None:
"""
Layer 1: Detect if the LAST candle (index -2, last closed) swept a swing level
with rejection (wick beyond level, close back inside).
Returns sweep info dict or None.
"""
if len(highs) < SWING_LB*2+5: return None
cur_h = highs[-2]; cur_l = lows[-2]; cur_c = closes[-2]; cur_o = opens[-2]
# Check last 30 bars for swing levels
h_slice = highs[-32:-2]; l_slice = lows[-32:-2]
sw_highs = detect_swing_highs(h_slice, SWING_LB)
sw_lows = detect_swing_lows(l_slice, SWING_LB)
# ── Bearish sweep: price wicks above a swing high but closes below ─────────
for idx, level in sw_highs[-3:]: # Check last 3 swing highs
if (cur_h > level + SWEEP_TOL # Wick penetrated
and (not REQ_REJECT or cur_c < level)): # Close back below
body_up = cur_h - max(cur_c, cur_o)
body_dn = min(cur_c, cur_o) - cur_l
rej_str = "strong" if body_up > (cur_h - cur_l)*0.3 else "weak"
return {
"type": "bearish",
"level": round(level,6),
"level_idx": idx,
"wick_high": cur_h,
"close": cur_c,
"rejection": rej_str,
"liq_type": "swing_high",
}
# ── Bullish sweep: price wicks below a swing low but closes above ──────────
for idx, level in sw_lows[-3:]:
if (cur_l < level - SWEEP_TOL
and (not REQ_REJECT or cur_c > level)):
body_dn = min(cur_c, cur_o) - cur_l
rej_str = "strong" if body_dn > (cur_h - cur_l)*0.3 else "weak"
return {
"type": "bullish",
"level": round(level,6),
"level_idx": idx,
"wick_low": cur_l,
"close": cur_c,
"rejection": rej_str,
"liq_type": "swing_low",
}
return None
# ── Layer 2: Fair Value Gap Detection ─────────────────────────────────────────
def detect_fvg(highs, lows, closes, sweep_type: str, sym) -> dict | None:
"""
Layer 2: After a sweep candle (index -2), scan the 3 most recent candles
for a Fair Value Gap 3-candle imbalance where candle 2 body doesn't
overlap candles 1 and 3's wicks.
Bullish FVG (after bullish sweep): Candle3.low > Candle1.high price void below
Bearish FVG (after bearish sweep): Candle3.high < Candle1.low price void above
Min gap = min_gap_pips
"""
min_gap = MIN_GAP_P * pip(sym)
n = len(highs)
if n < 4: return None
# Scan last FVG_AGE+3 bars for fresh FVGs
for i in range(n-4, max(n-FVG_AGE-4, 1), -1):
c1h, c1l = highs[i], lows[i]
c2h, c2l = highs[i+1], lows[i+1]
c3h, c3l = highs[i+2], lows[i+2]
if sweep_type == "bullish":
# Bullish FVG: gap between candle1 high and candle3 low
gap = c3l - c1h
if gap > min_gap:
mitigation = min(closes[-2:])
mitigated = mitigation <= c1h + gap/2
return {
"type": "bullish",
"high": round(c3l, 6),
"low": round(c1h, 6),
"midpoint": round((c3l+c1h)/2, 6),
"gap_pips": round(gap/pip(sym), 1),
"bar_index": i+1,
"age_bars": n-2-i,
"mitigated": mitigated,
"strength": 3 if gap>min_gap*2 else 2 if gap>min_gap*1.5 else 1,
}
elif sweep_type == "bearish":
# Bearish FVG: gap between candle3 high and candle1 low
gap = c1l - c3h
if gap > min_gap:
mitigation = max(closes[-2:])
mitigated = mitigation >= c3h + gap/2
return {
"type": "bearish",
"high": round(c1l, 6),
"low": round(c3h, 6),
"midpoint": round((c1l+c3h)/2, 6),
"gap_pips": round(gap/pip(sym), 1),
"bar_index": i+1,
"age_bars": n-2-i,
"mitigated": mitigated,
"strength": 3 if gap>min_gap*2 else 2 if gap>min_gap*1.5 else 1,
}
return None
# ── Layer 3: Order Block Detection ────────────────────────────────────────────
def detect_order_block(highs, lows, closes, opens, fvg: dict, sweep_type: str) -> dict | None:
"""
Layer 3: The Order Block is the LAST candle before the displacement move
that caused the FVG.
- Bullish OB: last down-close candle before the bullish displacement
- Bearish OB: last up-close candle before the bearish displacement
OB quality scored by body ratio, wick ratio, freshness.
"""
fvg_bar = fvg.get("bar_index", len(closes)-3)
search_start = max(0, fvg_bar - OB_AGE)
if sweep_type == "bullish":
# Find last bearish (down-close) candle before fvg_bar
for i in range(fvg_bar, search_start, -1):
if i >= len(closes): continue
if closes[i] < opens[i]: # Bearish candle
total_range = highs[i] - lows[i]
if total_range <= 0: continue
body = abs(closes[i] - opens[i])
wicks = total_range - body
body_r = body / total_range
wick_r = wicks / total_range
if body_r >= MIN_BODY and wick_r <= MAX_WICK:
qual = round(min(20, body_r*20 + (1-wick_r)*10 + max(0,10-(fvg_bar-i))), 1)
return {
"type": "bullish",
"high": round(highs[i],6),
"low": round(lows[i],6),
"open": round(opens[i],6),
"close": round(closes[i],6),
"body_ratio": round(body_r,2),
"wick_ratio": round(wick_r,2),
"quality": qual,
"bar_idx": i,
"age_bars": fvg_bar - i,
}
else:
# Find last bullish (up-close) candle before fvg_bar
for i in range(fvg_bar, search_start, -1):
if i >= len(closes): continue
if closes[i] > opens[i]:
total_range = highs[i] - lows[i]
if total_range <= 0: continue
body = abs(closes[i] - opens[i])
wicks = total_range - body
body_r = body / total_range
wick_r = wicks / total_range
if body_r >= MIN_BODY and wick_r <= MAX_WICK:
qual = round(min(20, body_r*20 + (1-wick_r)*10 + max(0,10-(fvg_bar-i))), 1)
return {
"type": "bearish",
"high": round(highs[i],6),
"low": round(lows[i],6),
"open": round(opens[i],6),
"close": round(closes[i],6),
"body_ratio": round(body_r,2),
"wick_ratio": round(wick_r,2),
"quality": qual,
"bar_idx": i,
"age_bars": fvg_bar - i,
}
return None
# ── Confluence Scoring (0-100) ─────────────────────────────────────────────────
def is_killzone() -> tuple[bool, str]:
now = datetime.now(timezone.utc)
hr = now.hour + now.minute/60
if KZ_EN:
if KZ_LON["start"] <= hr <= KZ_LON["end"]: return True, "London"
if KZ_NY["start"] <= hr <= KZ_NY["end"]: return True, "New York"
return False, ""
def score_confluence(sweep, fvg, ob, m15_aligns: bool) -> tuple[int, dict]:
SC = CFG["confluence"]["scoring"]
kz, kz_name = is_killzone()
s_sweep = SC["sweep_quality"] if sweep.get("rejection")=="strong" else int(SC["sweep_quality"]*0.6)
s_fvg = SC["fvg_presence"] if fvg.get("strength",0)>=2 else int(SC["fvg_presence"]*0.6)
s_ob = min(SC["ob_quality"], int(ob.get("quality",0)/20*SC["ob_quality"])) if ob else 0
s_bos = SC["bos_strength"] if m15_aligns else int(SC["bos_strength"]*0.6)
s_kz = SC["killzone"] if kz else 0
s_mtf = SC["mtf_confluence"] if m15_aligns else 0
s_fresh = SC["ob_freshness"] if ob and ob.get("age_bars",99)<5 else int(SC["ob_freshness"]*0.5) if ob and ob.get("age_bars",99)<10 else 0
total = s_sweep + s_fvg + s_ob + s_bos + s_kz + s_mtf + s_fresh
breakdown = {
"bos_strength": s_bos, "sweep_quality": s_sweep,
"fvg_presence": s_fvg, "ob_quality": s_ob,
"killzone": s_kz, "mtf_confluence": s_mtf,
"ob_freshness": s_fresh,
}
return min(100, total), breakdown
# ── M15 context check ─────────────────────────────────────────────────────────
def get_m15_context(sym: str, direction: str) -> bool:
"""Check if M15 trend aligns with intended trade direction."""
try:
bars = get_bars(sym, "M15", 30)
if len(bars) < 20: return True
import pandas as pd, ta
df = pd.DataFrame(bars)
df.columns = [c.lower() for c in df.columns]
df["close"] = df["close"].astype(float)
ema20 = ta.trend.ema_indicator(df["close"], window=20)
last_close = float(df["close"].iloc[-2])
last_ema = float(ema20.iloc[-2])
if direction == "bullish": return last_close > last_ema
else: return last_close < last_ema
except: return True # Default allow if unavailable
def is_trade_time():
now = datetime.now(timezone.utc)
wd, hr = now.weekday(), now.hour
if (wd==4 and hr>=22) or wd==5 or (wd==6 and hr<22): return False
return START_H <= hr < END_H
def check_daily(sym):
today = str(date.today())
k = f"{sym}_{today}"
return _daily.get(k, 0)
def inc_daily(sym):
today = str(date.today())
k = f"{sym}_{today}"
_daily[k] = _daily.get(k, 0) + 1
def has_zeus_position():
pos = bridge("/positions")
return isinstance(pos,list) and any("ZEUS" in str(p.get("comment","")).upper() for p in pos)
# ── Main analysis (three-layer sequential) ─────────────────────────────────────
def run_analysis(symbol: str) -> dict:
symbol = symbol.upper()
if not symbol.endswith("XX"): symbol += "xx"
symbol = symbol[:-2] + "xx"
log.info(f"=== Zeus ICT Analysis: {symbol} ===")
# ── Preflight ────────────────────────────────────────────────────
acc = bridge("/balance")
if "error" in acc: return {"action":"wait","reason":f"Bridge: {acc['error']}"}
equity = float(acc.get("equity",0))
if equity <= 0: return {"action":"wait","reason":"No equity."}
if has_zeus_position(): return {"action":"wait","reason":"Zeus position already open."}
if time.time() - _last_sig.get(symbol,0) < COOLDOWN:
rem = int(COOLDOWN-(time.time()-_last_sig.get(symbol,0)))
return {"action":"wait","reason":f"Cooldown: {rem}s"}
daily_count = check_daily(symbol)
if daily_count >= MAX_DT:
return {"action":"wait","reason":f"Max daily trades ({MAX_DT}) reached."}
if not is_trade_time():
return {"action":"wait","reason":f"Outside session (GMT {START_H}{END_H})."}
quote = bridge(f"/quote?symbol={symbol}")
if "error" in quote or not quote.get("bid"):
return {"action":"wait","reason":f"No quote for {symbol}."}
bid = float(quote["bid"]); ask = float(quote["ask"])
spread = to_pips(ask-bid, symbol)
if spread > MAX_SPREAD:
return {"action":"wait","reason":f"Spread {spread:.2f} > {MAX_SPREAD} pips."}
bars = get_bars(symbol, "M5", 100)
if len(bars) < 30:
return {"action":"wait","reason":"Insufficient M5 data for ICT detection."}
highs = [float(b.get("high",0)) for b in bars]
lows = [float(b.get("low",0)) for b in bars]
closes = [float(b.get("close",0)) for b in bars]
opens = [float(b.get("open",0)) for b in bars]
# ════════════════════════════════════════════════════════════════
# LAYER 1: LIQUIDITY SWEEP
# ════════════════════════════════════════════════════════════════
sweep = detect_liquidity_sweep(highs, lows, closes, opens, symbol)
if not sweep:
return {"action":"wait","reason":"No liquidity sweep detected on M5.",
"layer":"1/3 — sweep not found"}
sweep_type = sweep["type"] # "bullish" or "bearish"
log.info(f"Layer 1 PASS: {sweep_type} sweep at {sweep['level']}")
# ════════════════════════════════════════════════════════════════
# LAYER 2: FAIR VALUE GAP (must follow the sweep)
# ════════════════════════════════════════════════════════════════
fvg = detect_fvg(highs, lows, closes, sweep_type, symbol)
if not fvg:
return {"action":"wait","reason":"Sweep found but no FVG after displacement.",
"layer":"2/3 — FVG not found", "sweep":sweep}
if fvg.get("mitigated") and CFG["ict"]["fvg"]["require_unmitigated"]:
return {"action":"wait","reason":"FVG found but already mitigated.",
"layer":"2/3 — FVG mitigated", "sweep":sweep, "fvg":fvg}
log.info(f"Layer 2 PASS: {fvg['type']} FVG gap={fvg['gap_pips']}pips str={fvg['strength']}")
# ════════════════════════════════════════════════════════════════
# LAYER 3: ORDER BLOCK
# ════════════════════════════════════════════════════════════════
ob = detect_order_block(highs, lows, closes, opens, fvg, sweep_type)
if not ob:
return {"action":"wait","reason":"Sweep+FVG found but no valid Order Block.",
"layer":"3/3 — OB not found", "sweep":sweep, "fvg":fvg}
log.info(f"Layer 3 PASS: {ob['type']} OB quality={ob['quality']} age={ob['age_bars']}bars")
# ════════════════════════════════════════════════════════════════
# CONFLUENCE SCORING
# ════════════════════════════════════════════════════════════════
m15_ok = get_m15_context(symbol, sweep_type)
score, breakdown = score_confluence(sweep, fvg, ob, m15_ok)
kz_active, kz_name = is_killzone()
if score < MIN_SCORE:
return {"action":"wait","reason":f"All 3 layers passed but score {score} < {MIN_SCORE}.",
"score":score,"breakdown":breakdown,"sweep":sweep,"fvg":fvg,"ob":ob}
# ════════════════════════════════════════════════════════════════
# SIGNAL CONSTRUCTION
# ════════════════════════════════════════════════════════════════
direction = "Buy" if sweep_type=="bullish" else "Sell"
entry = ask if direction=="Buy" else bid
# SL: just below/above the Order Block
if direction == "Buy":
sl = round(ob["low"] - OB_BUF, 6)
tp = round(entry + abs(entry-sl)*TP_MULT, 6)
else:
sl = round(ob["high"] + OB_BUF, 6)
tp = round(entry - abs(sl-entry)*TP_MULT, 6)
sl_pips = to_pips(entry-sl, symbol)
tp_pips = to_pips(tp-entry, symbol)
rr = round(tp_pips/sl_pips, 2) if sl_pips>0 else 0
if rr < MIN_RR:
return {"action":"wait","reason":f"R:R {rr} < {MIN_RR}.",
"score":score,"sweep":sweep,"fvg":fvg,"ob":ob}
volume = calc_lot(equity, sl_pips, symbol)
_last_sig[symbol] = time.time()
inc_daily(symbol)
log.info(f"SIGNAL: {direction} {symbol} score={score} SL={sl} TP={tp} Vol={volume}")
return {
"action": "trade",
"strategy": "zeus-ict-smartmoney",
"signal_type": f"ICT_{'BULLISH' if direction=='Buy' else 'BEARISH'}_SETUP",
"symbol": symbol,
"direction": direction,
"entry": entry,
"stop_loss": sl,
"take_profit": tp,
"volume": volume,
"rr_ratio": rr,
"sl_pips": round(sl_pips,1),
"tp_pips": round(tp_pips,1),
"confidence": "high" if score>=80 else "medium",
"confidence_score": score,
"score_breakdown": breakdown,
"layers": {
"1_sweep": sweep,
"2_fvg": fvg,
"3_ob": ob,
},
"killzone": kz_name if kz_active else "none",
"m15_aligned": m15_ok,
"signal_schema": {
"strategy_id": "ZEUS-v1",
"magic_number": CFG["strategy"]["magic_number"],
"risk_percent": RISK_PCT,
"confidence_score": score,
"metadata": {
"liquidity_sweep": {"level":sweep["level"],"type":sweep["liq_type"]},
"fvg": {"type":fvg["type"],"high":fvg["high"],"low":fvg["low"],"strength":fvg["strength"]},
"order_block": {"price":ob["low"] if direction=="Buy" else ob["high"],
"quality_score":ob["quality"]},
"killzone_active": kz_name or "none",
"score_breakdown": breakdown,
}
},
"analysed_at": datetime.now(timezone.utc).isoformat(),
}
if __name__=="__main__":
import sys
sym = sys.argv[1] if len(sys.argv)>1 else "EURUSDxx"
print(json.dumps(run_analysis(sym), indent=2, default=str))
+298
View File
@@ -0,0 +1,298 @@
#!/usr/bin/env python3
"""GENESIS — Zeus Tool + Telegram Bot (Strategy G: ICT Smart Money)
CLI for Hermes autonomous use. Bot for monitoring Hermes runs, you watch.
"""
import sys, os, json, time, logging, threading
sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent))
from pathlib import Path
from datetime import datetime, timezone
import yaml, requests
CONFIG_PATH = Path(__file__).parent / "zeus_config.yaml"
if not CONFIG_PATH.exists():
CONFIG_PATH = Path(__file__).parents[2] / "configs" / "zeus_config.yaml"
with open(CONFIG_PATH, encoding="utf-8") as f: CFG = yaml.safe_load(f)
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TG_CHAT_ID = str(CFG["telegram"]["chat_id"])
STRATEGY = CFG["strategy"]["name"]
COMMENT = CFG["strategy"]["comment"]
SYMBOLS = CFG["symbols"]
# Resolve safe journal path (fallback to local logs/ if system dir not writable)
default_journal = CFG["journal"]["path"]
try:
Path(default_journal).parent.mkdir(parents=True, exist_ok=True)
JOURNAL = Path(default_journal)
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "zeus"
local_log_dir.mkdir(parents=True, exist_ok=True)
JOURNAL = local_log_dir / "trade_journal.jsonl"
# Resolve safe log path (fallback to local logs/ if system dir not writable)
default_log = "/var/log/zeus/zeus_bot.log"
try:
Path(default_log).parent.mkdir(parents=True, exist_ok=True)
log_file = default_log
except Exception:
local_log_dir = Path(__file__).parents[2] / "logs" / "zeus"
local_log_dir.mkdir(parents=True, exist_ok=True)
log_file = str(local_log_dir / "zeus_bot.log")
logging.basicConfig(
filename=log_file,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)
_pending: dict = {}
_lock = threading.Lock()
def tg_send(text):
try:
requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id":TG_CHAT_ID,"text":text,"parse_mode":"Markdown"},timeout=10)
except: pass
def tg_updates(offset=0):
try:
r=requests.get(f"https://api.telegram.org/bot{TG_TOKEN}/getUpdates",
params={"timeout":30,"offset":offset},timeout=40)
return r.json().get("result",[])
except: return []
def bridge(path,method="GET",data=None):
sys.path.insert(0, str(Path(__file__).parents[2] / "core"))
from mt5_bridge import bridge as _b
return _b(path,method,data)
def _load():
import importlib.util
spec=importlib.util.spec_from_file_location("zeus_cycle",Path(__file__).parent/"zeus_cycle.py")
mod=importlib.util.module_from_spec(spec); spec.loader.exec_module(mod); return mod
def do_analyze(sym):
try: return _load().run_analysis(sym)
except Exception as e: return {"action":"wait","reason":f"Error: {str(e)[:200]}"}
def do_execute(sym):
s=sym.upper(); s=(s+"xx") if not s.endswith("XX") else s
result=do_analyze(s)
if result.get("action")!="trade":
return {"executed":False,"reason":result.get("reason"),"layer":result.get("layer"),
"score":result.get("score")}
order=bridge("/market","POST",{"symbol":result["symbol"],"volume":result["volume"],
"type":result["direction"],"stop_loss":result["stop_loss"],
"take_profit":result["take_profit"],"comment":COMMENT})
ticket=order.get("ticket") or order.get("Ticket")
if ticket:
with open(JOURNAL,"a") as f:
f.write(json.dumps({"ticket":str(ticket),"symbol":result["symbol"],
"direction":result["direction"],"volume":result["volume"],
"sl":result["stop_loss"],"tp":result["take_profit"],
"entry":result["entry"],"rr":result["rr_ratio"],
"score":result["confidence_score"],"signal_type":result.get("signal_type"),
"killzone":result.get("killzone"),"opened":datetime.now(timezone.utc).isoformat(),
"result":None,"pnl":None,"strategy":"zeus-ict-smartmoney",
"triggered_by":"hermes-autonomous"})+"\n")
layers = result.get("layers",{})
sweep = layers.get("1_sweep",{}); fvg = layers.get("2_fvg",{}); ob = layers.get("3_ob",{})
tg_send(
f"⚡ *ZEUS TRADE — Hermes Triggered*\n"
f"`{result['symbol']}` {result['direction']} | {result.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{result['entry']}` SL: `{result['stop_loss']}` TP: `{result['take_profit']}`\n"
f"R:R: `{result['rr_ratio']}` | Vol: `{result['volume']}`\n"
f"🎯 Score: `{result['confidence_score']}/100` | {result.get('killzone','no KZ')}\n"
f"Layer 1: {sweep.get('type','?')} sweep @ {sweep.get('level','?')}\n"
f"Layer 2: {fvg.get('type','?')} FVG {fvg.get('gap_pips','?')}pips\n"
f"Layer 3: OB quality {ob.get('quality','?')}/20\n"
f"🔖 Ticket: `{ticket}`"
)
return {"executed":True,"ticket":str(ticket),"score":result["confidence_score"],
"direction":result["direction"],"rr":result["rr_ratio"]}
else:
err=order.get("message",str(order))
tg_send(f"⚠️ *ZEUS*: Order FAILED — `{err}`")
return {"executed":False,"reason":f"Order failed: {err}"}
def do_scan():
best=None; best_sc=0; results={}
for sym in SYMBOLS:
r=do_analyze(sym)
results[sym]={"action":r.get("action"),"score":r.get("confidence_score",0),
"layer":r.get("layer",""),"reason":r.get("reason","")[:80]}
if r.get("action")=="trade":
sc=r.get("confidence_score",0)
if sc>best_sc: best_sc=sc; best=r
time.sleep(0.5)
return {"best_signal":best,"scan_results":results,
"signals_found":sum(1 for r in results.values() if r["action"]=="trade")}
def do_status():
acc=bridge("/balance"); pos=bridge("/positions")
zeus_pos=None; all_pos=[]
if isinstance(pos,list):
for p in pos:
c=str(p.get("comment",""))
all_pos.append({"ticket":p.get("ticket"),"symbol":p.get("symbol"),
"type":p.get("orderType"),"profit":p.get("profit"),"comment":c})
if "ZEUS" in c.upper(): zeus_pos=p
w=l=0
if JOURNAL.exists():
for line in JOURNAL.read_text().strip().split("\n"):
if not line: continue
try:
t=json.loads(line)
if t.get("result")=="win": w+=1
if t.get("result")=="loss": l+=1
except: pass
return {"account":acc,"zeus_position":zeus_pos,"all_open":all_pos,
"journal":{"wins":w,"losses":l},"strategy":"ICT Smart Money M5"}
def do_close():
pos=bridge("/positions"); closed=[]
if isinstance(pos,list):
for p in pos:
if "ZEUS" in str(p.get("comment","")).upper():
bridge("/close","POST",{"ticket":p["ticket"]}); closed.append(p["ticket"])
tg_send(f"⚡ *ZEUS*: Position `{p['ticket']}` closed by Hermes.")
return {"closed":len(closed),"tickets":closed} if closed else {"closed":0,"reason":"No Zeus positions."}
def send_result(result, scan=False):
if result.get("action")!="trade":
layer = result.get("layer","?")
tg_send(f"⚡ *{STRATEGY}* — `{result.get('symbol','?')}`\n\nSignal: *WAIT*\n"
f"Progress: {layer}\n💡 {result.get('reason','')[:300]}")
return
with _lock: _pending.clear(); _pending.update(result)
layers = result.get("layers",{}); sc=result.get("score_breakdown",{})
sweep=layers.get("1_sweep",{}); fvg=layers.get("2_fvg",{}); ob=layers.get("3_ob",{})
kz_name=result.get("killzone","none")
tg_send(
f"⚡ *{STRATEGY} SIGNAL*{'_(scan best)_' if scan else ''} — `{result.get('symbol')}`\n\n"
f"Signal: *{result.get('direction')}* | {result.get('signal_type','').replace('_',' ')}\n"
f"Entry: `{result.get('entry')}` SL: `{result.get('stop_loss')}` TP: `{result.get('take_profit')}`\n"
f"R:R: `{result.get('rr_ratio')}` | Vol: `{result.get('volume')}`\n"
f"🎯 *Score: {result.get('confidence_score',0)}/100* | {'🕐 '+kz_name if kz_name!='none' else 'No KZ'}\n\n"
f"✅ *Three-Layer Confirmation:*\n"
f" Layer 1 Sweep: {sweep.get('type','?').upper()} @ {sweep.get('level','?')} ({sweep.get('rejection','?')})\n"
f" Layer 2 FVG: {fvg.get('type','?').upper()} {fvg.get('gap_pips','?')}pips str={fvg.get('strength','?')}\n"
f" Layer 3 OB: Quality {ob.get('quality','?')}/20 | Age {ob.get('age_bars','?')}bars\n\n"
f"📊 *Score Breakdown:*\n"
f" BOS:{sc.get('bos_strength',0)} Sweep:{sc.get('sweep_quality',0)} "
f"FVG:{sc.get('fvg_presence',0)} OB:{sc.get('ob_quality',0)}\n"
f" KZ:{sc.get('killzone',0)} MTF:{sc.get('mtf_confluence',0)} Fresh:{sc.get('ob_freshness',0)}\n\n"
f"`/zeus_execute` to trade | `/zeus_skip` to cancel"
)
def dispatch(text, from_id):
if str(from_id)!=TG_CHAT_ID: return
lower=text.lower().strip()
def bg(fn,*args): threading.Thread(target=fn,args=args,daemon=True).start()
if lower.startswith("/zeus_analyze"):
parts=text.split(maxsplit=1); sym=parts[1] if len(parts)>1 else ""
if not sym: tg_send("Usage: `/zeus_analyze EURUSD`"); return
def run():
tg_send(f"⚡ *{STRATEGY}*: Running 3-layer ICT analysis on `{sym.upper()}`… (3060s)")
send_result(do_analyze(sym))
bg(run)
elif lower=="/zeus_scan":
def run():
tg_send(f"⚡ *{STRATEGY}*: Scanning {len(SYMBOLS)} symbols (6090s)…")
r=do_scan()
lines=[]
for s,v in r["scan_results"].items():
if v["action"]=="trade": lines.append(f"⚡ `{s}`: Score {v['score']}/100")
else: lines.append(f"⚪ `{s}`: {v.get('layer','')}{v['reason'][:50]}")
tg_send("⚡ *ZEUS SCAN*\n\n"+"\n".join(lines))
if r["best_signal"]: send_result(r["best_signal"],scan=True)
else: tg_send(f"📊 No ICT setups found. Remember: 515 signals/month is normal.")
bg(run)
elif lower=="/zeus_execute":
def run():
with _lock:
if not _pending:
tg_send(f"⚡ No pending. Run `/zeus_scan` or `/zeus_analyze SYMBOL` first."); return
sig=dict(_pending); _pending.clear()
tg_send("⚡ Placing Zeus order…")
r=do_execute(sig.get("symbol","").replace("xx",""))
if not r.get("executed"): tg_send(f"❌ Failed: {r.get('reason')}")
bg(run)
elif lower=="/zeus_skip":
with _lock:
if not _pending: tg_send("⚡ No pending signal."); return
sym=_pending.get("symbol"); _pending.clear()
tg_send(f"⏭ *{STRATEGY}*: Signal for `{sym}` cancelled.")
elif lower=="/zeus_status":
def run():
r=do_status(); acc=r.get("account",{}); zp=r.get("zeus_position"); j=r.get("journal",{})
all_s="\n".join(f"`{p['symbol']}` {p['type']}{p.get('profit',0):.2f} [{p['comment']}]"
for p in r.get("all_open",[]))
tg_send(f"⚡ *{STRATEGY} — Status*\n\n"
f"💰 Balance: €{acc.get('balance',0):.2f} | Equity: €{acc.get('equity',0):.2f}\n"
f"📈 Zeus Position: {zp.get('symbol','None') if zp else 'None'}\n"
f"📒 Journal: {j.get('wins',0)}W / {j.get('losses',0)}L\n\n"
f"*All Open:*\n{all_s or 'None'}")
bg(run)
elif lower in ("/zeus_help","/zeus"):
tg_send(
f"⚡ *{STRATEGY} — Strategy G Commands*\n\n"
f"🤖 _Hermes runs Zeus autonomously._\n\n"
f"`/zeus_analyze [SYMBOL]` — 3-layer ICT analysis\n"
f"`/zeus_scan` — Scan all {len(SYMBOLS)} symbols\n"
f"`/zeus_execute` — Execute pending signal\n"
f"`/zeus_skip` — Cancel pending\n"
f"`/zeus_status` — Position + journal\n"
f"`/zeus_help` — This message\n\n"
f"📊 Three-Layer: Sweep → FVG → Order Block\n"
f"🕐 Killzones: London 0811 | NY 1316 GMT\n"
f"🔖 Tag: `{COMMENT}` | Risk: 0.75%\n"
f"⏱ Expected: 515 signals/month"
)
def cli():
args=sys.argv[1:]
if not args: print(json.dumps({"error":"Usage: zeus_tool.py [analyze|execute|scan|status|close|symbols] [SYMBOL]"})); sys.exit(1)
cmd=args[0].lower()
sym=args[1] if len(args)>1 else "EURUSDxx"
if cmd=="analyze": print(json.dumps(do_analyze(sym),indent=2,default=str))
elif cmd=="execute": print(json.dumps(do_execute(sym),indent=2,default=str))
elif cmd=="scan": print(json.dumps(do_scan(),indent=2,default=str))
elif cmd=="status": print(json.dumps(do_status(),indent=2,default=str))
elif cmd=="close": print(json.dumps(do_close(),indent=2,default=str))
elif cmd=="symbols": print(json.dumps({"symbols":SYMBOLS,"strategy":"ICT Smart Money","comment":COMMENT},indent=2))
else: print(json.dumps({"error":f"Unknown: {cmd}"}))
def bot():
log.info(f"=== {STRATEGY} Telegram Bot started ===")
tg_send(
f"⚡ *{STRATEGY} Bot Online*\n"
f"Strategy G: ICT Smart Money (Sweep → FVG → Order Block)\n"
f"Entry: M5 | Context: M15 | Killzones: London + NY\n"
f"Min Score: {CFG['confluence']['min_score']}/100 | Risk: 0.75%\n\n"
f"🤖 _Hermes controls this bot autonomously._\n"
f"_Expected: 515 signals/month. Quality over quantity._\n"
f"Send `/zeus_help` for commands."
)
offset=0
while True:
try:
for upd in tg_updates(offset):
offset=upd["update_id"]+1
msg=upd.get("message",{}); text=msg.get("text","")
chat_id=str(msg.get("chat",{}).get("id",""))
if text.startswith("/zeus"): dispatch(text,chat_id)
except Exception as e: log.error(f"Poll: {e}"); time.sleep(5)
time.sleep(1)
if __name__=="__main__":
if len(sys.argv) > 1 and sys.argv[1] == "bot":
bot()
elif Path(sys.argv[0]).name.startswith("zeus_telegram"):
bot()
else:
cli()