initial import
This commit is contained in:
commit
2fcbbfc3c9
32 changed files with 4998 additions and 0 deletions
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.devcontainer
|
||||||
|
.git
|
||||||
|
docker-compose.yaml
|
||||||
|
README.md
|
||||||
|
LICENSE.md
|
||||||
|
.gitignore
|
||||||
|
.env.example
|
||||||
|
.env
|
||||||
|
config
|
||||||
4
.env.example
Normal file
4
.env.example
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
UID=1000
|
||||||
|
GID=1000
|
||||||
|
CUSTOM_USER=username
|
||||||
|
PASSWORD=securepassword
|
||||||
112
.github/workflows/deploy.yml
vendored
Normal file
112
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
name: MT5 Helm GitOps Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
tags: [ "*" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Gleiche Registry wie im boday_tf-Projekt, zwei Namen:
|
||||||
|
# PUSH_REGISTRY: wie der Runner sie erreicht
|
||||||
|
# PULL_REGISTRY: wie k3s/containerd sie erreicht (registries.yaml, CA unter certs.d)
|
||||||
|
PUSH_REGISTRY: localhost:3005
|
||||||
|
PULL_REGISTRY: registry.local
|
||||||
|
# Host-Header für Traefik-Ingress des Web-UI (leer = kein Ingress, kein Health-Check per HTTP)
|
||||||
|
INGRESS_HOST: mt5.local
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
if: false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# HINWEIS: Terraform-Binary kommt wie bei boday_tf über das globale Runner-Volume (:ro)
|
||||||
|
|
||||||
|
- name: Trust Local Nginx Certificate Inside Container
|
||||||
|
run: |
|
||||||
|
mkdir -p /usr/local/share/ca-certificates/
|
||||||
|
cp nginx_ssl/nginx.crt /usr/local/share/ca-certificates/nginx-sandbox.crt 2>/dev/null || echo "Nutze Workspace-Zertifikat"
|
||||||
|
update-ca-certificates
|
||||||
|
|
||||||
|
# Docker verlangt Image-Namen in Kleinbuchstaben – "MetaTrader5-Docker-Image" würde sonst scheitern
|
||||||
|
- name: Compute Image Name
|
||||||
|
run: |
|
||||||
|
IMAGE_PATH="$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
|
||||||
|
echo "IMAGE=${PUSH_REGISTRY}/${IMAGE_PATH}" >> "$GITHUB_ENV"
|
||||||
|
echo "PULL_IMAGE=${PULL_REGISTRY}/${IMAGE_PATH}" >> "$GITHUB_ENV"
|
||||||
|
echo "Push: ${PUSH_REGISTRY}/${IMAGE_PATH}"
|
||||||
|
echo "Pull: ${PULL_REGISTRY}/${IMAGE_PATH}"
|
||||||
|
|
||||||
|
- name: Log in to Local Registry
|
||||||
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${PUSH_REGISTRY} -u ${{ github.actor }} --password-stdin
|
||||||
|
|
||||||
|
# Ein Build, zwei Tags (das Image ist ~4 GB – doppelt bauen kostet unnötig Zeit)
|
||||||
|
- name: Build MT5 Image
|
||||||
|
run: |
|
||||||
|
docker build --platform linux/amd64 \
|
||||||
|
-t ${IMAGE}:${{ github.sha }} \
|
||||||
|
-t ${IMAGE}:latest \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Push MT5 Image
|
||||||
|
run: |
|
||||||
|
docker push ${IMAGE}:${{ github.sha }}
|
||||||
|
docker push ${IMAGE}:latest
|
||||||
|
|
||||||
|
- name: Prepare Kubernetes Access
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.KUBECONFIG_DATA }}" > ./kind_kubeconfig
|
||||||
|
chmod 600 ./kind_kubeconfig
|
||||||
|
|
||||||
|
# Erster Rollout kann bis 15 Min dauern (Wine + MT5 Installation nach /config)
|
||||||
|
# init mit Retry, weil der DNS-Resolver sporadisch "server misbehaving" liefert
|
||||||
|
- name: Terraform Deploy
|
||||||
|
env:
|
||||||
|
# Web-UI-Login (KasmVNC) – wie bisher
|
||||||
|
TF_VAR_vnc_user: ${{ secrets.MT5_VNC_USER || 'trader' }}
|
||||||
|
TF_VAR_vnc_password: ${{ secrets.MT5_VNC_PASSWORD }}
|
||||||
|
# Broker-Login für MT5 – landet als login.ini im Pod und wird beim Start geladen
|
||||||
|
TF_VAR_mt5_login: ${{ secrets.MT5USER }}
|
||||||
|
TF_VAR_mt5_password: ${{ secrets.MT5PASSWORD }}
|
||||||
|
TF_VAR_mt5_server: ${{ secrets.MT5_SERVER }}
|
||||||
|
run: |
|
||||||
|
terraform --version
|
||||||
|
for i in 1 2 3; do
|
||||||
|
terraform init && break
|
||||||
|
if [ "$i" = "3" ]; then echo "terraform init nach 3 Versuchen fehlgeschlagen"; exit 1; fi
|
||||||
|
echo "init fehlgeschlagen, Versuch $i – neuer Anlauf in 10s"
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
terraform apply -auto-approve \
|
||||||
|
-var="image_repository=${PULL_IMAGE}" \
|
||||||
|
-var="app_version=${{ github.sha }}" \
|
||||||
|
-var="ingress_host=${INGRESS_HOST}"
|
||||||
|
|
||||||
|
# KasmVNC antwortet ohne Login mit 401 – daher mit Basic Auth prüfen und auf den Erststart warten
|
||||||
|
- name: Verify App Health
|
||||||
|
if: env.INGRESS_HOST != ''
|
||||||
|
env:
|
||||||
|
VNC_USER: ${{ secrets.MT5_VNC_USER || 'trader' }}
|
||||||
|
VNC_PASS: ${{ secrets.MT5_VNC_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
for i in $(seq 1 40); do
|
||||||
|
CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 \
|
||||||
|
-u "${VNC_USER}:${VNC_PASS}" -H "Host: ${INGRESS_HOST}" \
|
||||||
|
http://local-k3s-platform:80/ || true)
|
||||||
|
echo "Versuch ${i}: HTTP ${CODE}"
|
||||||
|
if [ "${CODE}" = "200" ]; then
|
||||||
|
echo "Erfolg: MetaTrader 5 Web-UI läuft via Helm im GitOps-Standard!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 15
|
||||||
|
done
|
||||||
|
echo "MT5 Web-UI nach 10 Minuten nicht erreichbar"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Cleanup Kubeconfig
|
||||||
|
if: always()
|
||||||
|
run: rm -f ./kind_kubeconfig
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
trader-data
|
||||||
|
config
|
||||||
|
.env
|
||||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
FROM ghcr.io/linuxserver/baseimage-kasmvnc:debianbookworm
|
||||||
|
|
||||||
|
# set version label
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG VERSION
|
||||||
|
LABEL build_version="Metatrader Docker:- ${VERSION} Build-date:- ${BUILD_DATE}"
|
||||||
|
LABEL maintainer="gmartin"
|
||||||
|
|
||||||
|
ENV TITLE=Metatrader5
|
||||||
|
ENV WINEPREFIX="/config/.wine"
|
||||||
|
ENV WINEDEBUG=-all
|
||||||
|
|
||||||
|
# Install all packages in a single layer to reduce image size
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
wget \
|
||||||
|
curl \
|
||||||
|
gnupg2 \
|
||||||
|
software-properties-common \
|
||||||
|
ca-certificates \
|
||||||
|
&& mkdir -pm755 /etc/apt/keyrings \
|
||||||
|
&& wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key \
|
||||||
|
&& wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/debian/dists/bookworm/winehq-bookworm.sources \
|
||||||
|
&& dpkg --add-architecture i386 \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install --install-recommends -y winehq-stable \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /etc/apt/keyrings/winehq-archive.key
|
||||||
|
|
||||||
|
|
||||||
|
COPY /Metatrader /Metatrader
|
||||||
|
RUN chmod +x /Metatrader/start.sh
|
||||||
|
COPY /root /
|
||||||
|
|
||||||
|
EXPOSE 3000 8001
|
||||||
|
VOLUME /config
|
||||||
22
LICENSE.md
Normal file
22
LICENSE.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2023 Germán Martín
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
175
Metatrader/start.sh
Executable file
175
Metatrader/start.sh
Executable file
|
|
@ -0,0 +1,175 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# MT5 im Container – Variante mit Broker-Terminal (PU Prime) und 64-Bit-Python.
|
||||||
|
# Gründe:
|
||||||
|
# - Das MetaTrader5-Python-Paket gibt es für 32-Bit-Python nur bis 5.0.37 (2022).
|
||||||
|
# Das hängt bei aktuellen Terminal-Builds in initialize(). 64-Bit-Python bekommt
|
||||||
|
# das aktuelle Paket.
|
||||||
|
# - Das Broker-Terminal bringt die PU-Prime-Server vorkonfiguriert mit.
|
||||||
|
|
||||||
|
# Configuration variables
|
||||||
|
WINEPREFIX='/config/.wine'
|
||||||
|
WINEDEBUG='-all'
|
||||||
|
wine_executable="wine"
|
||||||
|
mt5server_port="8001"
|
||||||
|
MT5_CMD_OPTIONS="${MT5_CMD_OPTIONS:-}"
|
||||||
|
mono_url="https://dl.winehq.org/wine/wine-mono/10.3.0/wine-mono-10.3.0-x86.msi"
|
||||||
|
python_url="https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe"
|
||||||
|
# Broker-Installer wird mit ins Image kopiert (Metatrader/vantageinternational5setup.exe).
|
||||||
|
# Fehlt er, wird das generische MetaQuotes-Terminal installiert.
|
||||||
|
broker_setup="/Metatrader/vantageinternational5setup.exe"
|
||||||
|
mt5setup_url="https://download.mql5.com/cdn/web/metaquotes.software.corp/mt5/mt5setup.exe"
|
||||||
|
program_files="/config/.wine/drive_c/Program Files"
|
||||||
|
|
||||||
|
# Function to display a graphical message
|
||||||
|
show_message() {
|
||||||
|
echo "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check if a dependency is installed
|
||||||
|
check_dependency() {
|
||||||
|
if ! command -v $1 &> /dev/null; then
|
||||||
|
echo "$1 is not installed. Please install it to continue."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# pip show statt pkg_resources (in neueren Pythons nicht mehr zuverlässig vorhanden)
|
||||||
|
is_python_package_installed() {
|
||||||
|
python3 -m pip show "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_wine_python_package_installed() {
|
||||||
|
$wine_executable python -m pip show "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Terminal suchen – Broker-Installer legen es unter eigenem Ordnernamen ab
|
||||||
|
find_terminal() {
|
||||||
|
find "$program_files" -maxdepth 2 -iname terminal64.exe 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ist das Python in Wine ein 64-Bit-Python?
|
||||||
|
wine_python_is_64bit() {
|
||||||
|
$wine_executable python -c "import struct,sys; sys.exit(struct.calcsize('P') != 8)" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for necessary dependencies
|
||||||
|
check_dependency "curl"
|
||||||
|
check_dependency "$wine_executable"
|
||||||
|
|
||||||
|
# Install Mono if not present
|
||||||
|
if [ ! -e "/config/.wine/drive_c/windows/mono" ]; then
|
||||||
|
show_message "[1/7] Downloading and installing Mono..."
|
||||||
|
curl -o /config/.wine/drive_c/mono.msi $mono_url
|
||||||
|
WINEDLLOVERRIDES=mscoree=d $wine_executable msiexec /i /config/.wine/drive_c/mono.msi /qn
|
||||||
|
rm /config/.wine/drive_c/mono.msi
|
||||||
|
show_message "[1/7] Mono installed."
|
||||||
|
else
|
||||||
|
show_message "[1/7] Mono is already installed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if MetaTrader 5 is already installed
|
||||||
|
mt5file="$(find_terminal)"
|
||||||
|
if [ -n "$mt5file" ]; then
|
||||||
|
show_message "[2/7] Terminal found: $mt5file"
|
||||||
|
else
|
||||||
|
show_message "[2/7] No MT5 terminal installed. Installing..."
|
||||||
|
|
||||||
|
# Set Windows 10 mode in Wine
|
||||||
|
$wine_executable reg add "HKEY_CURRENT_USER\\Software\\Wine" /v Version /t REG_SZ /d "win10" /f
|
||||||
|
|
||||||
|
if [ -f "$broker_setup" ]; then
|
||||||
|
show_message "[3/7] Using broker installer $broker_setup"
|
||||||
|
cp "$broker_setup" /config/.wine/drive_c/mt5setup.exe
|
||||||
|
else
|
||||||
|
show_message "[3/7] Downloading generic MT5 installer..."
|
||||||
|
curl -o /config/.wine/drive_c/mt5setup.exe $mt5setup_url
|
||||||
|
fi
|
||||||
|
show_message "[3/7] Installing MetaTrader 5..."
|
||||||
|
$wine_executable "/config/.wine/drive_c/mt5setup.exe" "/auto" &
|
||||||
|
wait
|
||||||
|
rm -f /config/.wine/drive_c/mt5setup.exe
|
||||||
|
|
||||||
|
# Manche Installer beenden sich, bevor der Download fertig ist – kurz warten
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
mt5file="$(find_terminal)"
|
||||||
|
[ -n "$mt5file" ] && break
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Recheck if MetaTrader 5 is installed
|
||||||
|
if [ -n "$mt5file" ] && [ -e "$mt5file" ]; then
|
||||||
|
show_message "[4/7] Terminal installed: $mt5file. Running MT5..."
|
||||||
|
$wine_executable "$mt5file" $MT5_CMD_OPTIONS &
|
||||||
|
else
|
||||||
|
show_message "[4/7] No terminal64.exe found under $program_files. MT5 cannot be run."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install 64-bit Python in Wine if not present
|
||||||
|
if wine_python_is_64bit; then
|
||||||
|
show_message "[5/7] 64-bit Python is already installed in Wine: $($wine_executable python --version 2>&1)"
|
||||||
|
else
|
||||||
|
if $wine_executable python --version >/dev/null 2>&1; then
|
||||||
|
show_message "[5/7] WARNING: Wine Python is 32-bit – installing 64-bit Python in addition."
|
||||||
|
show_message "[5/7] For a clean setup, delete ./config and start again."
|
||||||
|
fi
|
||||||
|
show_message "[5/7] Installing 64-bit Python in Wine..."
|
||||||
|
curl -L $python_url -o /tmp/python-installer.exe
|
||||||
|
$wine_executable /tmp/python-installer.exe /quiet InstallAllUsers=1 PrependPath=1 Include_test=0
|
||||||
|
rm /tmp/python-installer.exe
|
||||||
|
show_message "[5/7] Python installed in Wine: $($wine_executable python --version 2>&1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upgrade pip and install required packages
|
||||||
|
show_message "[6/7] Installing Python libraries"
|
||||||
|
$wine_executable python -m pip install --upgrade --no-cache-dir pip
|
||||||
|
|
||||||
|
# MetaTrader5: current release (supports current terminal builds and numpy 2)
|
||||||
|
show_message "[6/7] Installing MetaTrader5 library in Windows"
|
||||||
|
if ! is_wine_python_package_installed "MetaTrader5"; then
|
||||||
|
$wine_executable python -m pip install --no-cache-dir MetaTrader5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# RPyC server in Wine: mt5linux 0.1.9 (old CLI with -w), its own dependencies
|
||||||
|
# are ancient pins -> --no-deps, rpyc/plumbum installed explicitly
|
||||||
|
show_message "[6/7] Checking and installing mt5linux library in Windows if necessary"
|
||||||
|
if ! is_wine_python_package_installed "mt5linux"; then
|
||||||
|
$wine_executable python -m pip install --no-cache-dir --no-deps "mt5linux==0.1.9"
|
||||||
|
fi
|
||||||
|
if ! is_wine_python_package_installed "rpyc"; then
|
||||||
|
$wine_executable python -m pip install --no-cache-dir "rpyc>=6,<7" plumbum
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install python-dateutil if needed (datetime is built-in, but dateutil adds features)
|
||||||
|
if ! is_wine_python_package_installed "python-dateutil"; then
|
||||||
|
show_message "[6/7] Installing python-dateutil library in Windows"
|
||||||
|
$wine_executable python -m pip install --no-cache-dir python-dateutil
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install mt5linux library in Linux if not installed
|
||||||
|
show_message "[6/7] Checking and installing mt5linux library in Linux if necessary"
|
||||||
|
if ! is_python_package_installed "mt5linux"; then
|
||||||
|
pip install --break-system-packages --no-cache-dir --no-deps mt5linux==0.1.9 && \
|
||||||
|
pip install --break-system-packages --no-cache-dir "rpyc>=6,<7" plumbum numpy
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install pyxdg library in Linux if not installed
|
||||||
|
show_message "[6/7] Checking and installing pyxdg library in Linux if necessary"
|
||||||
|
if ! is_python_package_installed "pyxdg"; then
|
||||||
|
pip install --break-system-packages --no-cache-dir pyxdg
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the MT5 server on Linux (runs the RPyC classic server inside Wine Python)
|
||||||
|
show_message "[7/7] Starting the mt5linux server..."
|
||||||
|
python3 -m mt5linux --host 0.0.0.0 -p $mt5server_port -w $wine_executable python.exe &
|
||||||
|
|
||||||
|
# Give the server some time to start
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Check if the server is running
|
||||||
|
if ss -tuln | grep ":$mt5server_port" > /dev/null; then
|
||||||
|
show_message "[7/7] The mt5linux server is running on port $mt5server_port."
|
||||||
|
else
|
||||||
|
show_message "[7/7] Failed to start the mt5linux server on port $mt5server_port."
|
||||||
|
fi
|
||||||
BIN
Metatrader/vantageinternational5setup.exe
Normal file
BIN
Metatrader/vantageinternational5setup.exe
Normal file
Binary file not shown.
244
README.md
Normal file
244
README.md
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
# MetaTrader5 Docker Image
|
||||||
|
|
||||||
|
This project provides a Docker image for running MetaTrader5 with remote access via VNC, based on the [KasmVNC](https://github.com/kasmtech/KasmVNC) project and [KasmVNC Base Image from LinuxServer](https://github.com/linuxserver/docker-baseimage-kasmvnc).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Run MetaTrader5 in an isolated environment.
|
||||||
|
- Remote access to MetaTrader5 interface via an integrated VNC client accessible through a web browser.
|
||||||
|
- Built on the reliable and secure [KasmVNC](https://github.com/kasmtech/KasmVNC) project.
|
||||||
|
- RPyC server for remote access to Python MetaTrader Library from Windows or Linux using <https://github.com/lucas-campagna/mt5linux>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
----------
|
||||||
|
|
||||||
|
**NOTICE:**
|
||||||
|
Due to some compatibility issued, version 2 has switched its base from Alpine to Debian Linux. This and adding Python environment makes that container size is considerably bigger from about 600 MB to 4 GB.
|
||||||
|
|
||||||
|
If you just need to run Metatrader for running your MQL5 programs without any Python programming I recommend to go on using version 1.0. MetaTrader program is updated independently from image so you will always have latest MT5 version.
|
||||||
|
|
||||||
|
----------
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker installed on your machine.
|
||||||
|
- Only intelx86/amd64 host is supported
|
||||||
|
|
||||||
|
## Usage from repository
|
||||||
|
|
||||||
|
1. Clone this repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/gmag11/MetaTrader5-Docker-Image
|
||||||
|
cd MetaTrader5-Docker-Image
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build the Docker image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t mt5 .
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the Docker image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 3000:3000 -p 8001:8001 -v config:/config mt5
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can access MetaTrader5 via a web browser at localhost:3000.
|
||||||
|
|
||||||
|
On first run it may take a few minutes to get everything installed and running. Normally it takes less than 5 minutes. You don't need to do anything. All installation process is automatic and you should end up with MetaTrader5 running in your web session.
|
||||||
|
|
||||||
|
## Usage with docker compose with image form Docker Registry (preferred way)
|
||||||
|
|
||||||
|
1. Create a folder in a path where you have permission. For instance in your home.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir MT5
|
||||||
|
cd MT5
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `docker-compose.yaml` file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano docker-compose.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this content filling user and password with your own data.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mt5:
|
||||||
|
image: gmag11/metatrader5_vnc
|
||||||
|
container_name: mt5
|
||||||
|
volumes:
|
||||||
|
- ./config:/config
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
- 8001:8001
|
||||||
|
environment:
|
||||||
|
- CUSTOM_USER=<Choose a user>
|
||||||
|
- PASSWORD=<Choose a secure password>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notice**: If you do not need to do remote python programming you can get a much smaller installation changing this line:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
image: gmag11/metatrader5_vnc
|
||||||
|
```
|
||||||
|
|
||||||
|
by this one
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
image: gmag11/metatrader5_vnc:1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
----------
|
||||||
|
|
||||||
|
**Notice**: Due to Windows permission management, if you are using windows use Docker managed volume instead of bind mount. Use this compose file instead:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
mt5:
|
||||||
|
image: gmag11/metatrader5_vnc
|
||||||
|
container_name: mt5
|
||||||
|
volumes:
|
||||||
|
- mt5_config:/config
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
- 8001:8001
|
||||||
|
environment:
|
||||||
|
- CUSTOM_USER=<Choose a user>
|
||||||
|
- PASSWORD=<Choose a secure password>
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mt5_config:
|
||||||
|
```
|
||||||
|
|
||||||
|
----------
|
||||||
|
|
||||||
|
1. Start the container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
In some systems `docker compose` command does not exists. Try to use `docker-compose up -d` instead.
|
||||||
|
|
||||||
|
4. Connect to web interface
|
||||||
|
|
||||||
|
Start your browser pointing `http://<your ip address>:3000`
|
||||||
|
|
||||||
|
On first run it may take a few minutes to get everything installed and running. Normally it takes less than 5 minutes. You don't need to do anything. All installation process is automatic and you should end up with MetaTrader5 running in your web session.
|
||||||
|
|
||||||
|
## Where to place MQ5 and EX5 files
|
||||||
|
|
||||||
|
In the case you want to run your own MQL5 bots inside the container you can find MQL5 folder structure in
|
||||||
|
|
||||||
|
```bash
|
||||||
|
config/.wine/drive_c/Program Files/MetaTrader 5/MQL5
|
||||||
|
```
|
||||||
|
|
||||||
|
All files that you place there can be accessed from your MetaTrader container without the need to restart anything.
|
||||||
|
|
||||||
|
You can access MetaEditor program clicking in `IDE` button in MetaTrader5 interface.
|
||||||
|
|
||||||
|
**Notice**: If you will run MQL5 only bots (without Python) you can run perfectly with gmag11/metatrader5_vnc:1.0 image as pointed before. Remember that **image version is not stuck to a specific MetaTrader 5 version**.
|
||||||
|
|
||||||
|
**Metatrader will always be updated automatically to latest version as it does when it is nativelly installed in Windows.**
|
||||||
|
|
||||||
|
## Python programming
|
||||||
|
|
||||||
|
You need to install [mt5linux library](https://github.com/lucas-campagna/mt5linux) in your Python host. It may be in any OS, not only Linux.
|
||||||
|
|
||||||
|
This is a simple snippet to run your Python script fron any host
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mt5linux import MetaTrader5
|
||||||
|
mt5 = MetaTrader5(host='host running docker container',port=8001)
|
||||||
|
mt5.initialize()
|
||||||
|
print(mt5.version())
|
||||||
|
```
|
||||||
|
|
||||||
|
Output should be something like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
(mt5linux) linux:~/$ python3
|
||||||
|
Python 3.10.13 (main, Dec 26 2023, 20:21:41) [GCC 13.2.0] on linux
|
||||||
|
Type "help", "copyright", "credits" or "license" for more information.
|
||||||
|
>>> from mt5linux import MetaTrader5
|
||||||
|
>>> mt5 = MetaTrader5(host='192.168.1.10',port=8001)
|
||||||
|
>>> mt5.initialize()
|
||||||
|
True
|
||||||
|
>>> print(mt5.version())
|
||||||
|
(500, 4120, '22 Dec 2023')
|
||||||
|
>>>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The port configuration can be adjusted as per the instructions in the KasmVNC repository.
|
||||||
|
|
||||||
|
### MetaTrader 5 Command Line Options
|
||||||
|
|
||||||
|
You can pass command line options to MetaTrader 5 using the `MT5_CMD_OPTIONS` environment variable. This is useful for custom configurations, tester modes, or other MetaTrader command line parameters.
|
||||||
|
|
||||||
|
Example using docker-compose:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
mt5:
|
||||||
|
image: gmag11/metatrader5_vnc
|
||||||
|
container_name: mt5
|
||||||
|
volumes:
|
||||||
|
- mt5_config:/config
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
- 8001:8001
|
||||||
|
environment:
|
||||||
|
- CUSTOM_USER=<Choose a user>
|
||||||
|
- PASSWORD=<Choose a secure password>
|
||||||
|
- MT5_CMD_OPTIONS=/config:C:\\customized_for_tester_etc.ini
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mt5_config:
|
||||||
|
```
|
||||||
|
|
||||||
|
Example using docker run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 3000:3000 -p 8001:8001 \
|
||||||
|
-e MT5_CMD_OPTIONS="/config:C:\\customized_for_tester_etc.ini" \
|
||||||
|
-v mt5_config:/config \
|
||||||
|
gmag11/metatrader5_vnc
|
||||||
|
```
|
||||||
|
|
||||||
|
Common MetaTrader 5 command line options:
|
||||||
|
|
||||||
|
- `/config:<path>` - Use a specific configuration file
|
||||||
|
- `/login:<account>` - Automatically login to specified account
|
||||||
|
|
||||||
|
For a complete list of available options, refer to the [MetaTrader 5 documentation](https://www.metatrader5.com/en/terminal/help/start_advanced/start).
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
Feel free to contribute to this project. All contributions are welcome. Open an issue or create a pull request.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the terms of the [MIT license](https://opensource.org/license/mit/).
|
||||||
|
|
||||||
|
The [**KasmVNC**](https://github.com/kasmtech/KasmVNC) project is licensed under the [GNU General Public License v2.0 (GPLv2)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). You can check the license details of KasmVNC [here](https://github.com/kasmtech/KasmVNC/blob/master/LICENSE.TXT).
|
||||||
|
|
||||||
|
[**KasmVNC Base Image from LinuxServer**](https://github.com/linuxserver/docker-baseimage-kasmvnc) is licensed unther the GNU General Public License v3.0 (GPLv3). License is available [here](https://github.com/linuxserver/docker-baseimage-kasmvnc/blob/master/LICENSE)
|
||||||
|
|
||||||
|
Please ensure to comply with the terms and conditions of the licenses while using or modifying this project.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
Acknowledgments to the [KasmVNC](https://github.com/kasmtech/KasmVNC) project, [KasmVNC Base Image from LinuxServer](https://github.com/linuxserver/docker-baseimage-kasmvnc/tree/master), [mt5linux library](https://github.com/lucas-campagna/mt5linux) and any other project or individual that contributed to the realization of this project.
|
||||||
9
bots.env
Normal file
9
bots.env
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
MT5_LOGIN=36562757
|
||||||
|
MT5_PASSWORD=v*fa%F5m
|
||||||
|
MT5_SERVER=PUPrime-Live6
|
||||||
|
MT5_PATH=C:\Program Files\PU Prime MT5 Terminal\terminal64.exe
|
||||||
|
|
||||||
|
API_ID="28487993"
|
||||||
|
API_HASH="b14b06cec21a72b5cb2773ea782c7057"
|
||||||
|
PHONE_NUMBER=+41796600666
|
||||||
|
CHANNEL2_ID=-3908994316
|
||||||
13
bots/Dockerfile
Normal file
13
bots/Dockerfile
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Ein gemeinsames Image für mcp_server und live_signal_trader (~500 MB statt zwei)
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1 PIP_NO_CACHE_DIR=1
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
# mt5linux 1.1.x nutzt Python-3.12-Syntax und hat eine andere Server-CLI.
|
||||||
|
# 0.1.9 passt zum gmag11-Server; seine uralten Pins (numpy 1.21 …) daher --no-deps.
|
||||||
|
RUN pip install --no-deps mt5linux==0.1.9
|
||||||
|
|
||||||
|
COPY *.py ./
|
||||||
446
bots/hm_signal_parser.py
Normal file
446
bots/hm_signal_parser.py
Normal file
|
|
@ -0,0 +1,446 @@
|
||||||
|
"""
|
||||||
|
Parser for the HM XAUUSD Telegram channel (channel 2).
|
||||||
|
|
||||||
|
Message format (raw Telegram Web copy-paste):
|
||||||
|
|
||||||
|
HM XAUUSD:
|
||||||
|
SIGNAL ALERT
|
||||||
|
|
||||||
|
SELL XAUUSD 4145-4155
|
||||||
|
|
||||||
|
TP1: 4140
|
||||||
|
TP2: 4130
|
||||||
|
...
|
||||||
|
SL: 4165
|
||||||
|
|
||||||
|
HM XAUUSD:
|
||||||
|
TP1 hit, that's +70 pips
|
||||||
|
|
||||||
|
HM XAUUSD:
|
||||||
|
BIN AUF BREAK EVEN !!!
|
||||||
|
|
||||||
|
Key differences from channel 1 (Goldex):
|
||||||
|
- Messages are separated by the "HM XAUUSD:" sender prefix, not by dash lines
|
||||||
|
- No Date: / Message: wrapper in the raw copy-paste format
|
||||||
|
→ dates are assigned sequentially from an optional start timestamp,
|
||||||
|
or left as None when used purely from the live Telethon listener
|
||||||
|
(which always supplies real timestamps)
|
||||||
|
- "SIGNAL ALERT" header line precedes the actual signal body
|
||||||
|
- TP done: "TP1 hit, that's +N pips" (existing TP_DONE_RE already catches "hit")
|
||||||
|
- SL hit: "SL hit, that's -N pips" (existing SL_HIT_RE already catches this)
|
||||||
|
- BE: "BIN AUF BREAK EVEN" / "BIN AUF BE" (German) - NOT in base class
|
||||||
|
- Risk label at end of signal: "MID RISK" / "HIGH RISK" - stored as metadata
|
||||||
|
- REENTRY messages: skipped (treated as chatter, not a new signal)
|
||||||
|
- Informal: bare "SELL XAUUSD" / "BUY XAUUSD" with no price
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
from traderanalyser import (
|
||||||
|
TradeSignalAnalyzer,
|
||||||
|
TP_DONE_RE, TP_NUMBER_RE, ALL_TP_RE, PARTIAL_CLOSE_RE, CANCEL_RE, BE_MOVE_RE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extend the base SL_HIT regex to include German break-even phrases.
|
||||||
|
# The base class catches "sl hit", "stopped out", "stop loss hit", "be hit".
|
||||||
|
# HM uses "BIN AUF BREAK EVEN", "BIN AUF BE", and "BIN ALLE AUF BE RAUS"
|
||||||
|
# (= "I've exited all at breakeven"). The ALLE/RAUS variants insert words
|
||||||
|
# between BIN and AUF/BE, so we can't rely on word-adjacency alone.
|
||||||
|
HM_SL_HIT_RE = re.compile(
|
||||||
|
r'\bsl\s*hit\b'
|
||||||
|
r'|\bstopped\s*out\b'
|
||||||
|
r'|\bstop\s*loss\s*hit\b'
|
||||||
|
r'|\bbe\s*hit\b'
|
||||||
|
r'|bin\s+auf\s+break\s*even' # BIN AUF BREAK EVEN
|
||||||
|
r'|bin\s+auf\s+be\b' # BIN AUF BE
|
||||||
|
r'|bin\s+\w+\s+auf\s+be\b' # BIN ALLE AUF BE (one word between)
|
||||||
|
r'|auf\s+be\s+raus' # AUF BE RAUS (exited at BE)
|
||||||
|
r'|alle\s+(?:\w+\s+)*auf\s+be\b' # ALLE ... AUF BE
|
||||||
|
r'|sl\s+auf\s+(?:be|break\s*even)\b' # SL AUF BE / SL AUF BREAK EVEN
|
||||||
|
r'|jetzt\s+auf\s+be\b', # JETZT AUF BE (now to BE)
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# "REENTRY" lines are channel-specific chatter - not new signals, not
|
||||||
|
# management messages. We skip them by detecting the keyword early.
|
||||||
|
REENTRY_RE = re.compile(r'\breentry\b', re.IGNORECASE)
|
||||||
|
|
||||||
|
# "RUNNING X pips" - informational, skip
|
||||||
|
RUNNING_RE = re.compile(r'\brunning\b.*\bpips\b', re.IGNORECASE)
|
||||||
|
|
||||||
|
# Risk label at the end of a signal block
|
||||||
|
RISK_LABEL_RE = re.compile(r'\b(high|mid|low)\s+risk\b', re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
class HMTradeSignalAnalyzer(TradeSignalAnalyzer):
|
||||||
|
"""Subclass of TradeSignalAnalyzer wired to the HM XAUUSD channel format.
|
||||||
|
|
||||||
|
Overrides only what differs: the file-parsing entry point (messages
|
||||||
|
delimited by "HM XAUUSD:" prefixes rather than dash separators, no Date
|
||||||
|
field), BE detection (German phrases), and REENTRY/RUNNING skipping.
|
||||||
|
Everything else - entry/SL/TP extraction, sanity checks, simulation,
|
||||||
|
analysis - is inherited unchanged.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def parse_signals(self, start_date=None):
|
||||||
|
"""Parse a raw Telegram Web copy-paste file.
|
||||||
|
|
||||||
|
Because the copy-paste format carries no timestamps, synthetic dates
|
||||||
|
are assigned starting from `start_date` (default: 2026-01-01 UTC),
|
||||||
|
incrementing by 1 minute per message so the chronological sort in the
|
||||||
|
base class works correctly. When used from the live Telethon listener
|
||||||
|
(process_message called directly), real timestamps are always supplied
|
||||||
|
by Telethon and this method is never called.
|
||||||
|
"""
|
||||||
|
if start_date is None:
|
||||||
|
start_date = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
with open(self.file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Split on "HM XAUUSD:" prefix (the sender header Telegram Web inserts
|
||||||
|
# before each message). The pattern matches the prefix at the start of
|
||||||
|
# a line, optionally followed by a newline before the message body.
|
||||||
|
parts = re.split(r'(?m)^HM XAUUSD:\s*\n?', content)
|
||||||
|
|
||||||
|
parsed = []
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
msg = part.strip()
|
||||||
|
if not msg:
|
||||||
|
continue
|
||||||
|
# Skip sticker / photo placeholders
|
||||||
|
if re.match(r'^(😑\s*Sticker|🖼\s*Photo)$', msg, re.IGNORECASE):
|
||||||
|
continue
|
||||||
|
date = start_date + timedelta(minutes=i)
|
||||||
|
parsed.append((date, msg))
|
||||||
|
|
||||||
|
# Already in order (copy-paste is chronological), but sort anyway for
|
||||||
|
# consistency with the base class contract.
|
||||||
|
parsed.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
for date, msg in parsed:
|
||||||
|
self.process_message(date, msg)
|
||||||
|
|
||||||
|
self._drop_superseded_informal_signals()
|
||||||
|
|
||||||
|
def process_message(self, date, msg):
|
||||||
|
"""Same as base class but using the extended HM regexes."""
|
||||||
|
# REENTRY / RUNNING lines are chatter - skip before any other check
|
||||||
|
if REENTRY_RE.search(msg) or RUNNING_RE.search(msg):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extract optional risk label (stored on the signal for reference)
|
||||||
|
risk_match = RISK_LABEL_RE.search(msg)
|
||||||
|
risk_hint = risk_match.group(1).lower() if risk_match else None
|
||||||
|
|
||||||
|
# Strip the "SIGNAL ALERT" header line - it's just a preamble and
|
||||||
|
# confuses the entry-range extraction if left in.
|
||||||
|
msg_clean = re.sub(r'(?im)^\s*SIGNAL ALERT\s*$', '', msg).strip()
|
||||||
|
|
||||||
|
# HM uses "SELL XAUUSD 4145-4155" (action before pair).
|
||||||
|
# The base parser expects "XAUUSD SELL ..." so swap the order here.
|
||||||
|
msg_clean = re.sub(
|
||||||
|
r'(?i)\b(BUY|SELL)\s+(XAUUSD|GOLD|XAU|BTCUSD|BTC|XAGUSD|SILVER)\b',
|
||||||
|
r'\2 \1',
|
||||||
|
msg_clean,
|
||||||
|
)
|
||||||
|
|
||||||
|
signal = self._parse_new_trade(msg_clean, date)
|
||||||
|
if signal:
|
||||||
|
if risk_hint:
|
||||||
|
signal['risk_hint'] = risk_hint # informational metadata
|
||||||
|
self.signals.append(signal)
|
||||||
|
return signal
|
||||||
|
|
||||||
|
if not self.signals or self.signals[-1]['status'] != 'OPEN':
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Management messages - use the extended HM_SL_HIT_RE
|
||||||
|
if CANCEL_RE.search(msg):
|
||||||
|
self.signals[-1]['status'] = 'CANCELLED'
|
||||||
|
self.signals[-1]['cancel_date'] = date
|
||||||
|
self.signals[-1]['cancel_reason'] = msg.strip()
|
||||||
|
return 'CANCEL'
|
||||||
|
|
||||||
|
# BE-move messages: base class BE_MOVE_RE catches English phrases;
|
||||||
|
# HM adds German ones via HM_SL_HIT_RE (which doubles as BE detector).
|
||||||
|
# Check HM-specific German BE patterns first, then fall through to
|
||||||
|
# the base class pattern for shared English phrases.
|
||||||
|
HM_BE_MOVE_RE = re.compile(
|
||||||
|
r'bin\s+auf\s+break\s*even'
|
||||||
|
r'|bin\s+auf\s+be\b'
|
||||||
|
r'|bin\s+\w+\s+auf\s+be\b'
|
||||||
|
r'|auf\s+be\s+raus'
|
||||||
|
r'|alle\s+(?:\w+\s+)*auf\s+be\b'
|
||||||
|
r'|sl\s+auf\s+(?:be|break\s*even)\b'
|
||||||
|
r'|jetzt\s+auf\s+be\b',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if HM_BE_MOVE_RE.search(msg) or BE_MOVE_RE.search(msg):
|
||||||
|
# Record on the signal but keep it OPEN (position not closed yet)
|
||||||
|
self.signals[-1]['be_move_date'] = date
|
||||||
|
return 'BE_MOVE'
|
||||||
|
|
||||||
|
if HM_SL_HIT_RE.search(msg):
|
||||||
|
self._handle_sl_hit(date, msg)
|
||||||
|
elif ALL_TP_RE.search(msg):
|
||||||
|
self._handle_tp_report(date, msg, all_levels=True)
|
||||||
|
elif TP_NUMBER_RE.search(msg) and TP_DONE_RE.search(msg):
|
||||||
|
# "TP1 hit, that's +70 pips" → TP_NUMBER_RE finds "TP1", TP_DONE_RE finds "hit"
|
||||||
|
self._handle_tp_report(date, msg)
|
||||||
|
elif PARTIAL_CLOSE_RE.search(msg):
|
||||||
|
self._handle_partial_close(date, msg)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _handle_sl_hit(self, date, msg):
|
||||||
|
"""Override to also recognize German BE phrases as breakeven exits."""
|
||||||
|
sig = self.signals[-1]
|
||||||
|
sig['status'] = 'CLOSED'
|
||||||
|
# Reuse HM_SL_HIT_RE but distinguish BE vs SL close type.
|
||||||
|
# Any message that reaches here already matched HM_SL_HIT_RE.
|
||||||
|
# Classify as BE if the message mentions BE/break-even (not a plain SL).
|
||||||
|
be_hit = bool(re.search(
|
||||||
|
r'\bbe\s*hit\b'
|
||||||
|
r'|bin\s+auf\s+break\s*even'
|
||||||
|
r'|bin\s+auf\s+be\b'
|
||||||
|
r'|bin\s+\w+\s+auf\s+be\b'
|
||||||
|
r'|auf\s+be\s+raus'
|
||||||
|
r'|alle\s+(?:\w+\s+)*auf\s+be\b'
|
||||||
|
r'|sl\s+auf\s+(?:be|break\s*even)\b'
|
||||||
|
r'|jetzt\s+auf\s+be\b',
|
||||||
|
msg, re.IGNORECASE,
|
||||||
|
))
|
||||||
|
sig['reported_close_type'] = 'BE' if be_hit else 'SL'
|
||||||
|
sig['reported_close_date'] = date
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_from_telegram(analyzer, channel_id, days):
|
||||||
|
"""Pull `days` days of history from a Telegram channel and feed each
|
||||||
|
message through analyzer.process_message(). Returns (total_msgs, skipped).
|
||||||
|
|
||||||
|
Requires the same TELEGRAM_API_ID / TELEGRAM_API_HASH / PHONE_NUMBER env
|
||||||
|
vars as live_signal_trader.py. Uses a temporary session file so it never
|
||||||
|
conflicts with the live trader's session.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
from datetime import timedelta
|
||||||
|
from telethon.sync import TelegramClient
|
||||||
|
|
||||||
|
api_id = int(os.environ['API_ID'])
|
||||||
|
api_hash = os.environ['API_HASH']
|
||||||
|
phone = os.environ['PHONE_NUMBER']
|
||||||
|
|
||||||
|
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
||||||
|
print(f"Connecting to Telegram, fetching {days} days of history "
|
||||||
|
f"(since {cutoff.date()}) from channel {channel_id} …")
|
||||||
|
|
||||||
|
total_msgs = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
# Reuse the live trader's existing session so no re-authentication is needed.
|
||||||
|
session_name = 'mysession'
|
||||||
|
|
||||||
|
with TelegramClient(session_name, api_id, api_hash) as client:
|
||||||
|
client.start(phone=phone)
|
||||||
|
|
||||||
|
# Resolve the channel by walking iter_dialogs() — this gives us the
|
||||||
|
# fully resolved entity (with access hash) regardless of session cache.
|
||||||
|
# Telegram stores channel IDs as positive ints internally; the -100
|
||||||
|
# prefix is only a convention in client-facing representations.
|
||||||
|
bare_id = abs(channel_id)
|
||||||
|
if str(bare_id).startswith('100') and len(str(bare_id)) > 10:
|
||||||
|
bare_id = int(str(bare_id)[3:])
|
||||||
|
|
||||||
|
entity = None
|
||||||
|
print("Resolving channel from dialogs…")
|
||||||
|
for dialog in client.iter_dialogs():
|
||||||
|
if getattr(dialog.entity, 'id', None) == bare_id:
|
||||||
|
entity = dialog.entity
|
||||||
|
break
|
||||||
|
|
||||||
|
if entity is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Channel {channel_id} not found in your dialogs. "
|
||||||
|
"Make sure you are a member of the channel."
|
||||||
|
)
|
||||||
|
|
||||||
|
for msg in client.iter_messages(entity, reverse=True, offset_date=cutoff):
|
||||||
|
text = (msg.text or '').strip()
|
||||||
|
if not text:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
total_msgs += 1
|
||||||
|
analyzer.process_message(msg.date, text)
|
||||||
|
|
||||||
|
analyzer._drop_superseded_informal_signals()
|
||||||
|
return total_msgs, skipped
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Standalone backtester for the HM channel export.
|
||||||
|
|
||||||
|
File mode (default):
|
||||||
|
python3 hm_signal_parser.py <file> [mt5_url] [tier] [compound] [capital] [sizing]
|
||||||
|
|
||||||
|
Telegram mode (live history fetch – no file needed):
|
||||||
|
python3 hm_signal_parser.py --telegram [channel_id] [days] [mt5_url] [tier] [compound] [capital] [sizing]
|
||||||
|
|
||||||
|
In Telegram mode the script connects using the same TELEGRAM_API_ID /
|
||||||
|
TELEGRAM_API_HASH / PHONE_NUMBER env vars as the live trader, fetches the
|
||||||
|
last N days of history from the HM channel, feeds every message through the
|
||||||
|
parser, then runs the full backtest analysis — identical output to file mode.
|
||||||
|
|
||||||
|
channel_id defaults to the CHANNEL2_ID env var (the HM channel).
|
||||||
|
days defaults to 5.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from traderanalyser import RISK_TIERS, RISK_PCT_TIERS, MIN_LOT
|
||||||
|
|
||||||
|
# ── Telegram mode ────────────────────────────────────────────────────────
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == '--telegram':
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
argv = sys.argv[2:] # strip the flag
|
||||||
|
channel_id = int(argv[0]) if len(argv) > 0 else int(os.environ.get('CHANNEL2_ID', 0))
|
||||||
|
days = int(argv[1]) if len(argv) > 1 else 5
|
||||||
|
mt5_base_url = argv[2] if len(argv) > 2 else 'http://localhost:8000'
|
||||||
|
chosen_tier = argv[3] if len(argv) > 3 else 'mid'
|
||||||
|
compounding = argv[4].lower() in ('1', 'true', 'yes', 'compound') if len(argv) > 4 else False
|
||||||
|
initial_capital = float(argv[5]) if len(argv) > 5 else 100
|
||||||
|
sizing_mode = argv[6] if len(argv) > 6 else 'survival'
|
||||||
|
|
||||||
|
if not channel_id:
|
||||||
|
print("ERROR: channel_id required (pass as arg or set CHANNEL2_ID env var).")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Use a placeholder path – the file is never opened in Telegram mode.
|
||||||
|
analyzer = HMTradeSignalAnalyzer(
|
||||||
|
file_path='<telegram>',
|
||||||
|
initial_capital=initial_capital,
|
||||||
|
compounding=compounding,
|
||||||
|
mt5_base_url=mt5_base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_msgs, skipped = _fetch_from_telegram(analyzer, channel_id, days)
|
||||||
|
print(f"Fetched {total_msgs} messages ({skipped} empty/media skipped) "
|
||||||
|
f"from channel {channel_id} over the last {days} days.")
|
||||||
|
|
||||||
|
# ── File mode ────────────────────────────────────────────────────────────
|
||||||
|
else:
|
||||||
|
file_path = sys.argv[1] if len(sys.argv) > 1 else 'hm_messages.txt'
|
||||||
|
mt5_base_url = sys.argv[2] if len(sys.argv) > 2 else 'http://localhost:8000'
|
||||||
|
chosen_tier = sys.argv[3] if len(sys.argv) > 3 else 'mid'
|
||||||
|
compounding = sys.argv[4].lower() in ('1', 'true', 'yes', 'compound') if len(sys.argv) > 4 else False
|
||||||
|
initial_capital = float(sys.argv[5]) if len(sys.argv) > 5 else 100
|
||||||
|
sizing_mode = sys.argv[6] if len(sys.argv) > 6 else 'survival'
|
||||||
|
|
||||||
|
analyzer = HMTradeSignalAnalyzer(
|
||||||
|
file_path, initial_capital=initial_capital,
|
||||||
|
compounding=compounding, mt5_base_url=mt5_base_url,
|
||||||
|
)
|
||||||
|
analyzer.parse_signals()
|
||||||
|
|
||||||
|
if not analyzer.signals:
|
||||||
|
print("No signals found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if analyzer.skipped_signals:
|
||||||
|
print(f"\n{len(analyzer.skipped_signals)} signal(s) skipped by sanity check:")
|
||||||
|
for date, pair, entry, reason in analyzer.skipped_signals:
|
||||||
|
print(f" {date} {pair} entry={entry} ({reason})")
|
||||||
|
|
||||||
|
n_informal = sum(1 for s in analyzer.signals if s['is_informal'])
|
||||||
|
print(f"\n{len(analyzer.signals)} signals found ({n_informal} informal)")
|
||||||
|
|
||||||
|
# Show risk hint distribution
|
||||||
|
hints = [s.get('risk_hint') for s in analyzer.signals if s.get('risk_hint')]
|
||||||
|
if hints:
|
||||||
|
from collections import Counter
|
||||||
|
print(f"Risk hints in signals: {dict(Counter(hints))}")
|
||||||
|
|
||||||
|
analyzer.fetch_forex_data()
|
||||||
|
|
||||||
|
if analyzer.defaulted_signals:
|
||||||
|
for date, pair, action, field, note in analyzer.defaulted_signals:
|
||||||
|
print(f" {date} {pair} {action}: {note}")
|
||||||
|
|
||||||
|
mode_label = 'COMPOUNDING' if compounding else 'FIXED'
|
||||||
|
print(f"\nRisk tier comparison [{mode_label}]:")
|
||||||
|
print("-" * 70)
|
||||||
|
print(" FIXED table:")
|
||||||
|
for tier in ('high', 'mid', 'low'):
|
||||||
|
df = analyzer.analyze_trades(risk_level=tier, sizing_mode='fixed')
|
||||||
|
evaluated = df[df['result'] != 'NO_DATA']
|
||||||
|
final = df['capital'].iloc[-1] if not df.empty else initial_capital
|
||||||
|
ret = (final - initial_capital) / initial_capital * 100
|
||||||
|
wr = evaluated['pnl'].gt(0).mean() * 100 if not evaluated.empty else 0.0
|
||||||
|
print(f" {tier:>5}: final {final:,.2f} USDT return {ret:+.1f}% "
|
||||||
|
f"max dd {df['drawdown'].max():.1f}% win rate {wr:.1f}%")
|
||||||
|
|
||||||
|
print(" RISK_PCT table:")
|
||||||
|
for tier in ('high', 'mid', 'low'):
|
||||||
|
df = analyzer.analyze_trades(risk_level=tier, sizing_mode='risk_pct')
|
||||||
|
evaluated = df[df['result'] != 'NO_DATA']
|
||||||
|
final = df['capital'].iloc[-1] if not df.empty else initial_capital
|
||||||
|
ret = (final - initial_capital) / initial_capital * 100
|
||||||
|
wr = evaluated['pnl'].gt(0).mean() * 100 if not evaluated.empty else 0.0
|
||||||
|
print(f" {tier:>5} ({RISK_PCT_TIERS[tier]*100:.1f}%/trade): final {final:,.2f} USDT "
|
||||||
|
f"return {ret:+.1f}% max dd {df['drawdown'].max():.1f}% win rate {wr:.1f}%")
|
||||||
|
|
||||||
|
print(" SURVIVAL table (1/3 risk, TP1-only fallback when lot < 0.01):")
|
||||||
|
df_surv = analyzer.analyze_trades(sizing_mode='survival', compounding=True)
|
||||||
|
evaluated_surv = df_surv[df_surv['result'] != 'NO_DATA']
|
||||||
|
final_surv = df_surv['capital'].iloc[-1] if not df_surv.empty else initial_capital
|
||||||
|
ret_surv = (final_surv - initial_capital) / initial_capital * 100
|
||||||
|
wr_surv = evaluated_surv['pnl'].gt(0).mean() * 100 if not evaluated_surv.empty else 0.0
|
||||||
|
n_tp1 = (df_surv['sizing'].str.startswith('survival/TP1')).sum() if 'sizing' in df_surv.columns else '?'
|
||||||
|
print(f" (compounding): final {final_surv:,.2f} USDT return {ret_surv:+.1f}% "
|
||||||
|
f"max dd {df_surv['drawdown'].max():.1f}% win rate {wr_surv:.1f}% "
|
||||||
|
f"({n_tp1} trades in TP1-only fallback)")
|
||||||
|
|
||||||
|
results_df = analyzer.analyze_trades(
|
||||||
|
risk_level=chosen_tier, sizing_mode=sizing_mode, compounding=compounding,
|
||||||
|
)
|
||||||
|
|
||||||
|
if results_df.empty or 'capital' not in results_df.columns:
|
||||||
|
print("No trades could be evaluated.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\nDetailed results — '{chosen_tier}' tier, '{sizing_mode}' sizing:")
|
||||||
|
print("-" * 50)
|
||||||
|
final = results_df['capital'].iloc[-1]
|
||||||
|
print(f"Initial capital: {initial_capital} USDT")
|
||||||
|
print(f"Final capital: {final:.2f} USDT")
|
||||||
|
print(f"Total return: {(final - initial_capital) / initial_capital * 100:.2f}%")
|
||||||
|
print(f"Max drawdown: {results_df['drawdown'].max():.2f}%")
|
||||||
|
|
||||||
|
evaluated = results_df[results_df['result'] != 'NO_DATA']
|
||||||
|
print(f"Total signals: {len(results_df)} (no data: {(results_df['result'] == 'NO_DATA').sum()})")
|
||||||
|
if not evaluated.empty:
|
||||||
|
print(f"Win rate: {evaluated['pnl'].gt(0).mean()*100:.2f}%")
|
||||||
|
print(f"Best trade: {evaluated['pnl'].max():.2f} USDT")
|
||||||
|
print(f"Worst trade: {evaluated['pnl'].min():.2f} USDT")
|
||||||
|
print(f"Avg pnl/trade: {evaluated['pnl'].mean():.2f} USDT")
|
||||||
|
|
||||||
|
results_df.to_csv('hm_trading_results.csv', index=False)
|
||||||
|
print("\nFull per-trade results saved to hm_trading_results.csv")
|
||||||
|
|
||||||
|
base_cols = ['date', 'pair', 'action', 'entry', 'signal_type', 'lot',
|
||||||
|
'reported_result', 'result', 'pnl', 'pnl_pct', 'capital']
|
||||||
|
extra = ['sizing'] if sizing_mode == 'survival' and 'sizing' in results_df.columns else []
|
||||||
|
cols = [c for c in base_cols + extra if c in results_df.columns]
|
||||||
|
display_df = results_df[cols].copy()
|
||||||
|
if 'lot' in display_df.columns:
|
||||||
|
display_df['lot'] = display_df['lot'].round(4)
|
||||||
|
display_df['pnl'] = display_df['pnl'].round(2)
|
||||||
|
display_df['pnl_pct'] = display_df['pnl_pct'].round(2)
|
||||||
|
display_df['capital'] = display_df['capital'].round(2)
|
||||||
|
print("\nDetailed trade results:")
|
||||||
|
print(display_df.to_string())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
1215
bots/live_signal_trader.py
Normal file
1215
bots/live_signal_trader.py
Normal file
File diff suppressed because it is too large
Load diff
1095
bots/mcp_server.py
Normal file
1095
bots/mcp_server.py
Normal file
File diff suppressed because it is too large
Load diff
12
bots/requirements.txt
Normal file
12
bots/requirements.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# MCP-Server
|
||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
pydantic
|
||||||
|
# mt5linux wird im Dockerfile separat (0.1.9, --no-deps) installiert
|
||||||
|
rpyc>=6,<7 # muss zur rpyc-Version im MT5-Container passen
|
||||||
|
pandas
|
||||||
|
numpy
|
||||||
|
# Live-Trader
|
||||||
|
requests
|
||||||
|
telethon
|
||||||
|
python-dotenv
|
||||||
1059
bots/traderanalyser.py
Normal file
1059
bots/traderanalyser.py
Normal file
File diff suppressed because it is too large
Load diff
6
charts/mt5/Chart.yaml
Normal file
6
charts/mt5/Chart.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
apiVersion: v2
|
||||||
|
name: mt5
|
||||||
|
description: MetaTrader 5 mit KasmVNC-Webzugriff und RPyC (mt5linux) auf k3s
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: "latest"
|
||||||
32
charts/mt5/templates/_helpers.tpl
Normal file
32
charts/mt5/templates/_helpers.tpl
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{{- define "mt5.fullname" -}}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "mt5.labels" -}}
|
||||||
|
app.kubernetes.io/name: mt5
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/version: {{ .Values.image.tag | quote }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "mt5.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: mt5
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "mt5.secretName" -}}
|
||||||
|
{{- if .Values.auth.existingSecret -}}
|
||||||
|
{{ .Values.auth.existingSecret }}
|
||||||
|
{{- else -}}
|
||||||
|
{{ include "mt5.fullname" . }}-auth
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "mt5.claimName" -}}
|
||||||
|
{{- if .Values.persistence.existingClaim -}}
|
||||||
|
{{ .Values.persistence.existingClaim }}
|
||||||
|
{{- else -}}
|
||||||
|
{{ include "mt5.fullname" . }}-config
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
94
charts/mt5/templates/deployment.yaml
Normal file
94
charts/mt5/templates/deployment.yaml
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "mt5.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "mt5.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
# RWO-Volume + Wine-Prefix: nie zwei Pods gleichzeitig
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "mt5.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "mt5.selectorLabels" . | nindent 8 }}
|
||||||
|
annotations:
|
||||||
|
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
|
||||||
|
spec:
|
||||||
|
# Pod ist lange beschäftigt beim Herunterfahren von Wine
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- name: mt5
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: {{ include "mt5.secretName" . }}
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: {{ .Values.tz | quote }}
|
||||||
|
- name: PUID
|
||||||
|
value: {{ .Values.puid | quote }}
|
||||||
|
- name: PGID
|
||||||
|
value: {{ .Values.pgid | quote }}
|
||||||
|
{{- if .Values.mt5CmdOptions }}
|
||||||
|
- name: MT5_CMD_OPTIONS
|
||||||
|
value: {{ .Values.mt5CmdOptions | quote }}
|
||||||
|
{{- end }}
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
containerPort: 3000
|
||||||
|
protocol: TCP
|
||||||
|
- name: rpyc
|
||||||
|
containerPort: 8001
|
||||||
|
protocol: TCP
|
||||||
|
# Erststart installiert Wine/MT5 (~5 Min) – bis 15 Min Zeit geben
|
||||||
|
startupProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: web
|
||||||
|
periodSeconds: 10
|
||||||
|
failureThreshold: 90
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: web
|
||||||
|
periodSeconds: 15
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: web
|
||||||
|
periodSeconds: 30
|
||||||
|
failureThreshold: 5
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /config
|
||||||
|
- name: dshm
|
||||||
|
mountPath: /dev/shm
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "mt5.claimName" . }}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
- name: dshm
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
sizeLimit: {{ .Values.shmSize }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
32
charts/mt5/templates/ingress.yaml
Normal file
32
charts/mt5/templates/ingress.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{{- if and .Values.ingress.enabled .Values.ingress.host }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "mt5.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "mt5.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.ingress.className }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.ingress.host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
# Nur Web-UI (KasmVNC, inkl. WebSocket) – RPyC bleibt clusterintern
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "mt5.fullname" . }}
|
||||||
|
port:
|
||||||
|
name: web
|
||||||
|
{{- end }}
|
||||||
32
charts/mt5/templates/nano
Normal file
32
charts/mt5/templates/nano
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{{- if and .Values.ingress.enabled .Values.ingress.host }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "mt5.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "mt5.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.ingress.className }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
- host: {{ .Values.ingress.host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
# Nur Web-UI (KasmVNC, inkl. WebSocket) – RPyC bleibt clusterintern
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "mt5.fullname" . }}
|
||||||
|
port:
|
||||||
|
name: web
|
||||||
|
{{- end }}
|
||||||
20
charts/mt5/templates/pvc.yaml
Normal file
20
charts/mt5/templates/pvc.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ include "mt5.claimName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "mt5.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
# PVC bei helm uninstall behalten – sonst ist Wine-Prefix + Login weg
|
||||||
|
helm.sh/resource-policy: keep
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- {{ .Values.persistence.accessMode }}
|
||||||
|
{{- if .Values.persistence.storageClass }}
|
||||||
|
storageClassName: {{ .Values.persistence.storageClass }}
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.size }}
|
||||||
|
{{- end }}
|
||||||
15
charts/mt5/templates/secret.yaml
Normal file
15
charts/mt5/templates/secret.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{{- if not .Values.auth.existingSecret }}
|
||||||
|
{{- if not .Values.auth.password }}
|
||||||
|
{{- fail "auth.password muss gesetzt sein (oder auth.existingSecret verwenden)" }}
|
||||||
|
{{- end }}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "mt5.secretName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "mt5.labels" . | nindent 4 }}
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
CUSTOM_USER: {{ .Values.auth.user | quote }}
|
||||||
|
PASSWORD: {{ .Values.auth.password | quote }}
|
||||||
|
{{- end }}
|
||||||
19
charts/mt5/templates/service.yaml
Normal file
19
charts/mt5/templates/service.yaml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "mt5.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "mt5.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
selector:
|
||||||
|
{{- include "mt5.selectorLabels" . | nindent 4 }}
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
port: {{ .Values.service.webPort }}
|
||||||
|
targetPort: web
|
||||||
|
protocol: TCP
|
||||||
|
- name: rpyc
|
||||||
|
port: {{ .Values.service.rpycPort }}
|
||||||
|
targetPort: rpyc
|
||||||
|
protocol: TCP
|
||||||
63
charts/mt5/values.yaml
Normal file
63
charts/mt5/values.yaml
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
image:
|
||||||
|
# Upstream-Image (Debian, ~4 GB, inkl. Python/RPyC). "1.1" = schlanke Variante ohne Python.
|
||||||
|
# Eigenes Build aus dem ausgecheckten Repo: repository auf deine Registry setzen.
|
||||||
|
repository: gmag11/metatrader5_vnc
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
# KasmVNC-Login für das Web-UI
|
||||||
|
auth:
|
||||||
|
user: trader
|
||||||
|
password: "" # wird von Terraform via set_sensitive gesetzt
|
||||||
|
existingSecret: "" # alternativ: eigenes Secret mit Keys CUSTOM_USER / PASSWORD
|
||||||
|
|
||||||
|
# Zusätzliche MT5-Kommandozeilenoptionen, z.B. "/config:C:\\tester.ini" oder "/login:12345"
|
||||||
|
mt5CmdOptions: ""
|
||||||
|
|
||||||
|
# Zeitzone für Container (linuxserver-Basis)
|
||||||
|
tz: Europe/Zurich
|
||||||
|
puid: "1000"
|
||||||
|
pgid: "1000"
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
webPort: 3000 # KasmVNC Web-UI
|
||||||
|
rpycPort: 8001 # mt5linux RPyC – unauthentifiziert, nur clusterintern nutzen!
|
||||||
|
|
||||||
|
# Nur das Web-UI wird über Ingress exponiert, RPyC bewusst nicht
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
className: nginx
|
||||||
|
host: ""
|
||||||
|
# KasmVNC nutzt WebSockets – lange Timeouts, damit die Session nicht nach 60s abreißt
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||||
|
tls: []
|
||||||
|
# - secretName: mt5-tls
|
||||||
|
# hosts: [mt5.example.ch]
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
storageClass: local-path # k3s-Default
|
||||||
|
size: 20Gi # Wine-Prefix + MT5 + History-Daten
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
existingClaim: ""
|
||||||
|
|
||||||
|
# Chromium/Wine/KasmVNC brauchen mehr als die Default-64MB /dev/shm
|
||||||
|
shmSize: 1Gi
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 1Gi
|
||||||
|
limits:
|
||||||
|
memory: 4Gi
|
||||||
|
|
||||||
|
# MT5/Wine-Image ist nur amd64
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/arch: amd64
|
||||||
|
|
||||||
|
tolerations: []
|
||||||
|
affinity: {}
|
||||||
35
destroy-tf-force.sh
Executable file
35
destroy-tf-force.sh
Executable file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Räumt den MT5-Helm-Release hart aus dem K3s-Cluster.
|
||||||
|
#
|
||||||
|
# ./destroy-tf-force.sh -> alles weg, PVC (Wine-Prefix, MT5-Login, History) bleibt
|
||||||
|
# ./destroy-tf-force.sh --with-data -> zusätzlich PVC löschen (MT5 installiert sich neu)
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
RELEASE="mt5"
|
||||||
|
NAMESPACE="trading"
|
||||||
|
K="docker exec local-k3s-platform kubectl"
|
||||||
|
|
||||||
|
RESOURCES="ingress,service,deployment,statefulset,replicaset,pod,configmap,secret"
|
||||||
|
if [ "${1:-}" = "--with-data" ]; then
|
||||||
|
RESOURCES="${RESOURCES},pvc"
|
||||||
|
echo "!!! --with-data: PVC ${RELEASE}-config wird mitgelöscht"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "1. Entferne alle physischen K3s-Ressourcen von '${RELEASE}' in '${NAMESPACE}'..."
|
||||||
|
$K delete ${RESOURCES} -l app.kubernetes.io/instance=${RELEASE} -n ${NAMESPACE} \
|
||||||
|
--ignore-not-found=true --force --grace-period=0
|
||||||
|
|
||||||
|
echo "2. Lösche das geheime Helm-Release-Protokoll aus der K3s-Datenbank..."
|
||||||
|
# Helm speichert Releases als Secrets mit dem Typ 'sh.helm.release.v1...'
|
||||||
|
$K delete secret -n ${NAMESPACE} -l owner=helm,name=${RELEASE} \
|
||||||
|
--ignore-not-found=true --force --grace-period=0
|
||||||
|
|
||||||
|
echo "3. Atempause für die Cluster-API..."
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "4. Kontrolle – hier sollte nichts mehr von '${RELEASE}' stehen:"
|
||||||
|
$K get all,ingress,secret,pvc -n ${NAMESPACE} -l app.kubernetes.io/instance=${RELEASE} 2>/dev/null
|
||||||
|
$K get secret -n ${NAMESPACE} -l owner=helm,name=${RELEASE} 2>/dev/null
|
||||||
|
|
||||||
|
echo "Cluster ist absolut jungfräulich sauber!"
|
||||||
14
docker-compose-windows.yaml
Normal file
14
docker-compose-windows.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
services:
|
||||||
|
mt5:
|
||||||
|
build: .
|
||||||
|
image: gmag11/metatrader5_vnc
|
||||||
|
container_name: mt5
|
||||||
|
volumes:
|
||||||
|
- mt5-config:/config
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
- 8001:8001
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
mt5-config:
|
||||||
65
docker-compose.yaml
Normal file
65
docker-compose.yaml
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
services:
|
||||||
|
# 1. MetaTrader 5 (Wine + KasmVNC) – startet selbst den RPyC-Server auf 8001
|
||||||
|
mt5:
|
||||||
|
build: . # eigenes Dockerfile mit Fix
|
||||||
|
image: mt5-vnc:local # eigener Name – nicht mit dem Upstream-Image verwechseln
|
||||||
|
container_name: mt5
|
||||||
|
volumes:
|
||||||
|
- ./config:/config
|
||||||
|
ports:
|
||||||
|
- 3001:3000 # Web-UI
|
||||||
|
# 8001 (RPyC) bewusst NICHT veröffentlicht – ungeschützter Vollzugriff auf MT5
|
||||||
|
env_file:
|
||||||
|
- .env # CUSTOM_USER / PASSWORD = VNC-Login fürs Web-UI
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8001"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 10m # Erststart installiert Wine + MT5
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# 2. MCP-Server (FastAPI) – spricht per RPyC mit MT5, Broker-Login aus bots.env
|
||||||
|
mt5-mcp:
|
||||||
|
build: ./bots
|
||||||
|
image: mt5-bots:local
|
||||||
|
container_name: mt5-mcp
|
||||||
|
command: ["python", "mcp_server.py", "--live", "--rpyc-host", "mt5", "--rpyc-port", "8001"]
|
||||||
|
env_file:
|
||||||
|
- bots.env
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:8000:8000 # nur lokal – hat Order-Endpunkte ohne Auth
|
||||||
|
depends_on:
|
||||||
|
mt5:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c",
|
||||||
|
"import json,urllib.request,sys; d=json.load(urllib.request.urlopen('http://127.0.0.1:8000/health',timeout=5)); sys.exit(0 if d.get('connected') else 1)"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
start_period: 2m
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# 3. Telegram Live-Trader – handelt über den MCP-Server
|
||||||
|
live-trader:
|
||||||
|
image: mt5-bots:local
|
||||||
|
container_name: live-trader
|
||||||
|
working_dir: /data # Session + diagnostics_*.json landen im Volume
|
||||||
|
# Nur Kanal 2 (HM XAUUSD) – CHANNEL2_ID kommt aus bots.env
|
||||||
|
command: >
|
||||||
|
python /app/live_signal_trader.py --live
|
||||||
|
--channels ch1
|
||||||
|
--risk-tier low
|
||||||
|
--mt5-base-url http://mt5-mcp:8000
|
||||||
|
--diagnostics-dir /data
|
||||||
|
env_file:
|
||||||
|
- bots.env
|
||||||
|
environment:
|
||||||
|
- TG_SESSION=/data/mysession
|
||||||
|
volumes:
|
||||||
|
- ./trader-data:/data
|
||||||
|
depends_on:
|
||||||
|
mt5-mcp:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
105
main.tf
Normal file
105
main.tf
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
kubernetes = {
|
||||||
|
source = "hashicorp/kubernetes"
|
||||||
|
version = "~> 2.26"
|
||||||
|
}
|
||||||
|
helm = {
|
||||||
|
source = "hashicorp/helm"
|
||||||
|
version = "~> 2.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Eigener State-Pfad, damit sich MT5 und die Webseite nicht in die Quere kommen
|
||||||
|
backend "consul" {
|
||||||
|
address = "consul:8500"
|
||||||
|
path = "terraform/states/mt5_tf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Variablen ---------------------------------------------------------------
|
||||||
|
|
||||||
|
variable "app_version" {
|
||||||
|
type = string
|
||||||
|
default = "latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "image_repository" {
|
||||||
|
type = string
|
||||||
|
default = "gmag11/metatrader5_vnc"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vnc_user" {
|
||||||
|
type = string
|
||||||
|
default = "trader"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kommt aus TF_VAR_vnc_password (Secret MT5_VNC_PASSWORD)
|
||||||
|
variable "vnc_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ingress_host" {
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Provider ----------------------------------------------------------------
|
||||||
|
|
||||||
|
provider "kubernetes" {
|
||||||
|
config_path = "./kind_kubeconfig"
|
||||||
|
host = "https://local-k3s-platform:6443"
|
||||||
|
insecure = true
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "helm" {
|
||||||
|
kubernetes {
|
||||||
|
config_path = "./kind_kubeconfig"
|
||||||
|
host = "https://local-k3s-platform:6443"
|
||||||
|
insecure = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- MetaTrader 5 Deployment -------------------------------------------------
|
||||||
|
|
||||||
|
resource "helm_release" "mt5" {
|
||||||
|
name = "mt5"
|
||||||
|
chart = "./charts/mt5"
|
||||||
|
namespace = "trading"
|
||||||
|
create_namespace = true
|
||||||
|
|
||||||
|
# Erster Start installiert Wine + MT5 nach /config – das dauert ein paar Minuten
|
||||||
|
timeout = 900
|
||||||
|
wait = true
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "image.repository"
|
||||||
|
value = var.image_repository
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "image.tag"
|
||||||
|
value = var.app_version
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "auth.user"
|
||||||
|
value = var.vnc_user
|
||||||
|
}
|
||||||
|
|
||||||
|
set_sensitive {
|
||||||
|
name = "auth.password"
|
||||||
|
value = var.vnc_password
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "ingress.enabled"
|
||||||
|
value = var.ingress_host != "" ? "true" : "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "ingress.host"
|
||||||
|
value = var.ingress_host
|
||||||
|
}
|
||||||
|
}
|
||||||
1
reg_tele_dry.sh
Executable file
1
reg_tele_dry.sh
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
docker compose run --rm -it --no-deps live-trader python /app/live_signal_trader.py --dry --channels ch2
|
||||||
1
root/defaults/autostart
Normal file
1
root/defaults/autostart
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/Metatrader/start.sh
|
||||||
7
root/defaults/menu.xml
Normal file
7
root/defaults/menu.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<openbox_menu xmlns="http://openbox.org/3.4/menu">
|
||||||
|
<menu id="root-menu" label="MENU">
|
||||||
|
<item label="xterm" icon="/usr/share/pixmaps/xterm-color_48x48.xpm"><action name="Execute"><command>/usr/bin/xterm</command></action></item>
|
||||||
|
<item label="Metatrader 5" icon="/config/.wine/drive_c/Program Files/MetaTrader 5/Terminal.ico"><action name="Execute"><command>/usr/bin/wine "/config/.wine/drive_c/Program Files/MetaTrader 5/terminal64.exe"</command></action></item>
|
||||||
|
</menu>
|
||||||
|
</openbox_menu>
|
||||||
Loading…
Reference in a new issue