# TD Slope Params Server

Serves per-symbol, per-week trading parameters to the TDSlopeEA family,
gated by the same license-validity concept already used by
`LicenseValidator.mqh`. This is the server half of the "EA fetches its
own config instead of hardcoded input defaults" project — no MT5 code has
been touched yet; this is deliberately the first piece, so the wire
contract is solid before any EA/indicator changes are made.

## Status: tested, not yet deployed

Every code path below has been exercised over real HTTP in this session
(valid request, bad signature, unknown symbol, invalid license, stale/
replayed timestamp) and the `WalkForward_NextWeek_*.txt` parser has been
validated against your actual USDJPY output file — it reproduces, field
for field, the same mapping we derived by hand earlier in this project.
It has NOT been deployed anywhere or wired to your real license database.

## Files

| File | Purpose |
|---|---|
| `config.py` | Shared secret, timing windows, data paths. **Edit before deploying** — see below. |
| `crypto_utils.py` | HMAC-SHA256 signing/verification, timestamp helpers. |
| `license_check.py` | **STUB.** Pluggable license-validity interface — see "Needs your input" below. |
| `symbol_resolver.py` | Maps whatever symbol string the EA sends to a canonical Pepperstone name, via `data/symbol_synonyms.json`. |
| `params_store.py` | Finds the right week's JSON file for a symbol on disk. |
| `main.py` | The actual FastAPI app — the `/params` endpoint. |
| `parse_walkforward_txt.py` | Parses `WalkForward_NextWeek_*.txt` into the EA-input-name-mapped JSON structure this server publishes. |
| `publish_params.py` | CLI to actually publish a week's params (from a walk-forward txt file, or hand-built JSON). |
| `data/symbol_synonyms.json` | Alias -> canonical symbol table (starts empty; most instruments likely need no entry at all, see below). |
| `data/params/{SYMBOL}/{week-start}.json` | Where published params actually live. |

## How it fits with LicenseValidator.mqh

This deliberately mirrors the existing license-check wire protocol rather
than inventing something unrelated:

- Same request shape: `k` (license key), `m` (machine ID), `t` (timestamp),
  `b` (build string), plus a new `sym` field — and `s`, an HMAC-SHA256
  signature over `k|m|t|b|sym` (the existing scheme signs `k|m|t|b`; this
  just appends the new field rather than restructuring anything).
- Same idea of a signed response so a malformed/spoofed reply can't
  silently get applied — but unlike the license endpoint's compact
  pipe-delimited response, this one returns real JSON, with the actual
  params payload embedded as an escaped JSON *string* inside an outer
  envelope (see the big comment at the top of `main.py` for why — short
  version: it lets the MT5 client verify the signature by hashing exact
  literal text, with no risk of a JSON-canonicalization mismatch between
  Python and a hand-written MQL5 serializer).
- Reuses the **same failure-code vocabulary** (`INVALID`, `EXPIRED`,
  `COOLDOWN`, `INACTIVE`) for license-related rejections, so the EA can
  reuse its existing `_LV_ShowError()`-style alert handling instead of
  inventing a parallel one.
- Uses a **separate shared secret** from the license endpoint, on
  purpose — see the comment block at the top of `config.py` for the
  reasoning (blast-radius isolation). Easy to change to reuse the
  existing secret instead if you'd rather — just say so.

## Needs your input before this is deployable

1. **`license_check.py` is a stub.** It currently accepts any license key
   over 10 characters — obviously not real. It needs to be wired to
   whatever your actual license database is (I don't have visibility
   into that system, only the MQL5 client side of it). The interface
   (`LicenseStore.check(license_key, machine_id) -> LicenseCheckResult`)
   is designed to be a thin wrapper around whatever query your existing
   `/validate` endpoint's server-side code already runs — using the exact
   same source of truth matters, so this endpoint can never disagree with
   the main license check for the same (key, machine) pair.

2. **The shared secret in `config.py` is a placeholder.** Needs a real
   random value before deployment, set via the `TD_PARAMS_SHARED_SECRET`
   environment variable (never commit the real value to source control).
   Once you've got one, the corresponding MQL5-side obfuscated constant
   (matching the `_LV_OE`/`_LV_OS` pattern) is a separate, later step —
   not done yet since we're keeping server and client work separate.

3. **Deployment specifics** — is this meant to run as its own process
   alongside the existing license server on Corsair (matching the
   Apache+nginx transitional setup), or should it be merged into
   whatever process already serves `/validate`? Either works with this
   code as written; I don't know which you'd prefer.

## Running it locally (for testing)

```bash
pip install -r requirements.txt
export TD_PARAMS_SHARED_SECRET="some-real-random-value"
uvicorn main:app --host 127.0.0.1 --port 8123
```

Publish a week's params from a walk-forward output file:

```bash
python3 publish_params.py --symbol USDJPY \
    --from-walkforward-txt WalkForward_NextWeek_20260629_215934.txt
```

## Symbol synonyms

`data/symbol_synonyms.json` maps *aliases* to canonical (Pepperstone)
names. Since the EA's `_Symbol` should already match Pepperstone's own
naming in the normal case, **most instruments probably need no entry at
all** — the resolver treats an unmapped incoming symbol as already
canonical (identity fallback). Only add entries for actual mismatches
(broker suffix variants, human-friendly aliases someone might use when
manually publishing, etc).

## Open design questions not yet resolved

These don't block the server code as written, but are worth deciding
before the EA-side work starts:

- What should `publish_params.py` do automatically at the end of your
  weekly walk-forward run — is it called directly by that pipeline, or
  is publishing still a manual step for now?
- Should there be an admin/auth-gated endpoint to publish over HTTP
  (useful if Corsair and wherever the walk-forward Python runs aren't
  the same machine), or is direct filesystem/CLI access on the same box
  always going to be sufficient?
