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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .agents/skills/mt5-httpapi/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,56 @@ curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/history/deals?from=

Deal fields: `type` (0=buy, 1=sell), `entry` (0=opening, 1=closing), `profit` (0 for entries, realized P&L for exits).

### Chart Deployments

Deploy Expert Advisors to charts over HTTP — no RDP, no terminal restart.
Stage `.ex5` + `.set` files, declare deployments, and a resident loader EA
inside the terminal reconciles charts to match. The API holds desired state;
the loader reports observed truth. A deployment only flips to `running` once
the loader confirms the expert is live on a chart.

**Setup:** automatic via `[StartUp] Expert=` on boot. No manual attach needed.
Disable per terminal with `chartctl: false`.

Endpoint reference:

| Method | Endpoint | Description |
| ------ | -------- | ----------- |
| `POST` / `GET` / `DELETE` | `/experts` `/experts/<hash>` | Stage, list, remove EA `.ex5` |
| `POST` / `GET` | `/sets` `/sets/<name>` | Stage, list, inspect `.set` (returns parsed inputs) |
| `POST` / `GET` | `/deployments` | Create or list deployments |
| `GET` / `PATCH` / `DELETE` | `/deployments/<id>` | Inspect, pause/resume, change set, delete |
| `POST` | `/deployments/reconcile` | Force immediate reconcile |
| `GET` | `/charts` | Live chart/EA inventory |
| `GET` | `/loader` | Loader EA status and version |
| `POST` | `/charts/<id>/screenshot` | Capture chart as PNG |
| `POST` | `/charts/<id>/close` | Close a chart by id |

```bash
# Stage artifacts
curl -F "expert=@HappyGoldScalp.ex5" "$MT5_API_URL/experts"
curl -F "set=@gold-m5.set" "$MT5_API_URL/sets"

# Deploy
curl -X POST "$MT5_API_URL/deployments" \
-H "Authorization: Bearer $MT5_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"expert":"HappyGoldScalp.ex5","set":"gold-m5.set","symbol":"XAUUSD","timeframe":"M5"}'

# Check status
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/deployments"

# Pause / resume / delete
curl -X PATCH "$MT5_API_URL/deployments/dep_a1b2c3" \
-H "Authorization: Bearer $MT5_API_TOKEN" \
-d '{"enabled":false}'
curl -X DELETE "$MT5_API_URL/deployments/dep_a1b2c3" \
-H "Authorization: Bearer $MT5_API_TOKEN"
```

Full protocol: [`docs/chart-control-protocol.md`](docs/chart-control-protocol.md).
WebRequest allowlist provisioning: `GET/PUT /webrequest`, `POST /webrequest/apply`.

### Backtest

Run MT5 Strategy Tester via the API. Two-stage workflow: build the INI from a
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ mt5installers/*
assets/experts/*
!assets/experts/.gitkeep
!assets/experts/MT5SystemWarmup.mq5
!assets/experts/MT5ChartLoader.mq5
!assets/experts/include
!assets/experts/include/ChartControl.mqh
assets/sets/*
!assets/sets/.gitkeep
config/config.yaml
Expand Down
126 changes: 14 additions & 112 deletions CHANGELOG.md

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Run multiple brokers, accounts, and cloned terminal instances at the same time.
- **Built-in technical analysis** — [wickworks](https://github.com/psyb0t/docker-wickworks) calculates indicators and SMC primitives server-side without exposing its own port.
- **MCP for the robot overlords** — one endpoint per terminal or one unified endpoint for the whole pile, plus Claude Code, Codex, and OpenClaw integrations.
- **Optional multi-VM fuckery** — spread terminals across VMs or NUMA nodes, then reach them through Tailscale or Cloudflare Tunnel if you need to.
- **Chart Deployments** for remote EA deployment: stage `.ex5` + `.set` files, declare a deployment (symbol + timeframe), and a resident loader EA inside the terminal reconciles charts automatically — no RDP or manual attach needed.

## Requirements

Expand Down Expand Up @@ -129,6 +130,7 @@ The old README became a massive wall of API shit, so the details now live in sep
| Copy working curl and Go examples instead of guessing | [Clients and examples](docs/clients-and-examples.md) |
| Operate the bastard: Make targets, ports, remote access, concurrency, and logs | [Operations](docs/operations.md) |
| Split terminals across several Windows VMs or NUMA nodes | [Multi-VM setup](docs/multi-vm-setup.md) |
| Deploy EAs to charts remotely over HTTP | [Chart Deployments (chartctl)](docs/chart-control-protocol.md) |

## API at a glance

Expand All @@ -153,6 +155,23 @@ curl -H "Authorization: Bearer $MT5_API_TOKEN" \

That is just the hello-world shit. The [REST API docs](docs/rest-api.md) link to every endpoint and response shape.

**Chart Deployments** endpoints (live-mode terminals only; gated by `chartctl.enabled`):

```bash
# Stage an expert and set file, then deploy
curl -H "Authorization: Bearer $MT5_API_TOKEN" -F "expert=@MyEA.ex5" "$MT5_API_URL/experts"
curl -H "Authorization: Bearer $MT5_API_TOKEN" -F "set=@params.set" "$MT5_API_URL/sets"
curl -H "Authorization: Bearer $MT5_API_TOKEN" -X POST "$MT5_API_URL/deployments" \
-H "Content-Type: application/json" \
-d '{"expert":"MyEA.ex5","set":"params.set","symbol":"EURUSD","timeframe":"M15"}'

# Verify — status flips to "running" once the loader confirms attach
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/deployments"
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/loader"
```

Full protocol contract at [`docs/chart-control-protocol.md`](docs/chart-control-protocol.md).

## Development

```bash
Expand Down
Binary file added assets/autoit/AutoIt3_x64.exe
Binary file not shown.
124 changes: 124 additions & 0 deletions assets/autoit/inspect_options.au3
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
; inspect_options.au3 <window_match> <logpath>
; Non-destructive inspector for MT5's Tools->Options dialog. NO #includes
; (an undefined include function pops a blocking error dialog). Finds the MT5
; window containing <window_match> (the login), opens Options (Ctrl+O), walks
; every tab, and logs the VISIBLE controls (ClassNN, pos, text) so we can
; identify the Expert Advisors tab index + the WebRequest checkbox/list, then
; cancels with Esc.

Opt("WinTitleMatchMode", 2)
Opt("SendKeyDelay", 15)

Global $gLog = -1

Func LogW($s)
If $gLog <> -1 Then FileWrite($gLog, $s & @CRLF)
EndFunc

Func DumpVisible($hWin, $tag)
LogW("--- " & $tag & " ---")
Local $cl = WinGetClassList($hWin)
Local $arr = StringSplit(StringStripCR($cl), @LF)
Local $seen = "|"
For $i = 1 To $arr[0]
Local $c = $arr[$i]
If $c = "" Then ContinueLoop
If StringInStr($seen, "|" & $c & "|") > 0 Then ContinueLoop
$seen &= $c & "|"
For $n = 1 To 80
Local $ctrl = $c & $n
Local $pos = ControlGetPos($hWin, "", $ctrl)
If @error Then ExitLoop
Local $vis = ControlCommand($hWin, "", $ctrl, "IsVisible", "")
If $vis = 1 Then
Local $txt = ControlGetText($hWin, "", $ctrl)
LogW(" " & $ctrl _
& " xywh=" & $pos[0] & "," & $pos[1] & "," & $pos[2] & "," & $pos[3] _
& " text='" & StringLeft(StringReplace($txt, @CRLF, " "), 70) & "'")
EndIf
Next
Next
EndFunc

; ---- args ----
; 3 args: match, pid, logpath. 2 args (legacy): match, logpath.
If $CmdLine[0] < 2 Then Exit 10
Global $match = $CmdLine[1]
Global $pid = 0
Global $logpath
If $CmdLine[0] >= 3 Then
$pid = Int($CmdLine[2])
$logpath = $CmdLine[3]
Else
$logpath = $CmdLine[2]
EndIf

$gLog = FileOpen($logpath, 2)
If $gLog = -1 Then Exit 11
LogW("=== inspect_options match=" & $match & " pid=" & $pid & " ===")

; ---- find + activate MT5 (visible windows only; by owner pid when known —
; same-login terminal clones share the exact same window title) ----
Local $wl = WinList()
Local $hMT5 = 0
For $i = 1 To $wl[0][0]
Local $h = $wl[$i][1]
If $wl[$i][0] = "" Then ContinueLoop
If BitAND(WinGetState($h), 2) = 0 Then ContinueLoop
If $pid > 0 And WinGetProcess($h) <> $pid Then ContinueLoop
If StringInStr($wl[$i][0], $match) > 0 Then $hMT5 = $h
Next
If $hMT5 = 0 Then
LogW("RESULT=FAIL reason=mt5_window_not_found")
FileClose($gLog)
Exit 2
EndIf
WinActivate($hMT5)
Sleep(600)
LogW("mt5='" & WinGetTitle($hMT5) & "' win_pid=" & WinGetProcess($hMT5))

; ---- open Options ----
Send("^o")
Local $hOpt = WinWait("Options", "", 10)
If $hOpt <> 0 And $pid > 0 And WinGetProcess($hOpt) <> $pid Then $hOpt = 0
If $hOpt = 0 Then
LogW("RESULT=FAIL reason=options_not_found")
FileClose($gLog)
Exit 3
EndIf
WinActivate($hOpt)
Sleep(300)
LogW("options='" & WinGetTitle($hOpt) & "'")
LogW("classlist=" & StringReplace(WinGetClassList($hOpt), @LF, " | "))

; ---- find tab control, walk tabs ----
Local $tabClass = ""
Local $cls = StringSplit(StringStripCR(WinGetClassList($hOpt)), @LF)
For $i = 1 To $cls[0]
If StringInStr($cls[$i], "SysTabControl32") > 0 Then
$tabClass = $cls[$i]
ExitLoop
EndIf
Next
LogW("tabClass='" & $tabClass & "'")

If $tabClass <> "" Then
Local $cnt = ControlCommand($hOpt, "", $tabClass, "GetItemCount", "")
LogW("tab_count=" & $cnt)
If $cnt >= 1 Then
For $t = 1 To $cnt
ControlCommand($hOpt, "", $tabClass, "CurrentTab", $t)
Sleep(250)
DumpVisible($hOpt, "tab#" & $t)
Next
Else
DumpVisible($hOpt, "single")
EndIf
Else
DumpVisible($hOpt, "no-tabctl")
EndIf

LogW("RESULT=OK")
Send("{ESC}")
FileClose($gLog)
Exit 0
60 changes: 60 additions & 0 deletions assets/autoit/selftest.au3
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
; selftest.au3 <match> [<pid>] <logpath>
; Minimal, NO external includes. Confirms AutoIt runs at all, reports whether
; it's elevated, and whether it can find + drive the (elevated) MT5 window.
; With 3 args the second is the terminal64.exe pid (0 = unknown) — same-login
; terminal clones share the exact same window title, so only the pid pins one.

Opt("WinTitleMatchMode", 2)
Opt("SendKeyDelay", 15)

Local $match = ($CmdLine[0] >= 1) ? $CmdLine[1] : "?"
Local $pid = 0
Local $logpath = @ScriptDir & "\selftest.log"
If $CmdLine[0] >= 3 Then
$pid = Int($CmdLine[2])
$logpath = $CmdLine[3]
ElseIf $CmdLine[0] = 2 Then
$logpath = $CmdLine[2]
EndIf

Local $h = FileOpen($logpath, 2)
If $h = -1 Then Exit 11
FileWrite($h, "started args=" & $CmdLine[0] & " match=" & $match & " pid=" & $pid & @CRLF)
FileWrite($h, "IsAdmin=" & IsAdmin() & @CRLF)

Local $wl = WinList()
FileWrite($h, "windows_total=" & $wl[0][0] & @CRLF)
Local $hMT5 = 0
For $i = 1 To $wl[0][0]
If $wl[$i][0] <> "" And StringInStr($wl[$i][0], $match) > 0 Then
FileWrite($h, " match_win='" & $wl[$i][0] & "' visible=" _
& (BitAND(WinGetState($wl[$i][1]), 2) > 0 ? 1 : 0) _
& " pid=" & WinGetProcess($wl[$i][1]) & @CRLF)
If BitAND(WinGetState($wl[$i][1]), 2) = 0 Then ContinueLoop
If $pid > 0 And WinGetProcess($wl[$i][1]) <> $pid Then ContinueLoop
$hMT5 = $wl[$i][1]
EndIf
Next
If $hMT5 = 0 Then
FileWrite($h, "RESULT=FAIL reason=no_mt5_window" & @CRLF)
FileClose($h)
Exit 2
EndIf

WinActivate($hMT5)
Sleep(600)
FileWrite($h, "active_title='" & WinGetTitle("[ACTIVE]") & "'" & @CRLF)

Send("^o")
Local $hOpt = WinWait("Options", "", 8)
If $hOpt <> 0 And $pid > 0 And WinGetProcess($hOpt) <> $pid Then $hOpt = 0
If $hOpt = 0 Then
FileWrite($h, "RESULT=FAIL reason=options_did_not_open (input blocked? UIPI/elevation)" & @CRLF)
FileClose($h)
Exit 3
EndIf
FileWrite($h, "options_opened title='" & WinGetTitle($hOpt) & "'" & @CRLF)
Send("{ESC}")
FileWrite($h, "RESULT=OK" & @CRLF)
FileClose($h)
Exit 0
Loading