Parcourir la source

Prepare Hermes-compatible strategy contract

Lukas Goldschmidt il y a 3 semaines
Parent
commit
67f19d8341

+ 260 - 0
Hermes_Compatibility_Plan.md

@@ -0,0 +1,260 @@
+# Trader-MCP Hermes Compatibility Plan
+
+This file turns `TODO.md` into a stepwise upgrade path toward `hermes-mcp` compatibility.
+
+## Goal
+
+Prepare `trader-mcp` so Hermes can supervise strategy selection, risk posture, and switching without collapsing Trader into a monolith.
+
+Trader should remain:
+- strategy runtime
+- execution feedback surface
+- operator dashboard
+- persistence owner for strategy state
+
+Hermes should remain:
+- selector
+- supervisor
+- regime interpreter
+- switch/risk controller
+
+## Core design principles
+
+1. Small interfaces.
+2. Explicit ownership.
+3. Human-readable strategy names.
+4. Structured state over vibes.
+5. Stable switch semantics.
+6. Contract-first development.
+7. Tests before expansion.
+
+## Canonical strategy set
+
+Start with a small orthogonal registry:
+
+- `idle`
+- `defensive`
+- `trend_following`
+- `mean_reversion`
+- `grid`
+- `breakout`
+- `event_driven`
+
+### Strategy ordering priority
+
+1. `idle`
+2. `defensive`
+3. `trend_following`
+4. `mean_reversion`
+5. `grid`
+6. `breakout`
+7. `event_driven`
+
+This is not a quality ranking. It is a sequencing plan for implementation and testing.
+
+## Ownership boundaries
+
+### Strategy owns
+- its local logic
+- its local state
+- its local config
+- its own render output
+- its own explanation string
+
+### Trader runtime owns
+- lifecycle
+- persistence
+- snapshots
+- reconciliation
+- dashboard presentation
+- execution plumbing
+
+### Human observer owns
+- inspection
+- approval where configured
+- operational overrides where explicitly allowed
+- debugging and review
+
+### Hermes owns
+- strategy enable/disable/switch decisions
+- risk-mode changes
+- uncertainty and confidence weighting
+- regime interpretation
+- execution-degradation-driven downranking
+
+## Upgrade phases
+
+### Phase 0, document the contract
+Deliverables:
+- `Hermes_Trader_Contract_v0.1.md`
+- agreed strategy taxonomy
+- agreed state and decision fields
+- agreed control verbs
+
+Acceptance criteria:
+- every strategy has `expects`, `avoids`, and `risk_profile`
+- Hermes request and Trader response shapes are machine readable
+- no contract depends on hidden state
+
+### Phase 1, normalize the strategy registry
+Deliverables:
+- fold redundant names into the canonical set
+- mark parameter variants as modes
+- ensure strategy labels are stable and human-readable
+
+Acceptance criteria:
+- each strategy name maps to one conceptual regime
+- no micro-strategy sprawl
+- old names can be redirected or deprecated cleanly
+
+### Phase 2, expose Hermes-facing inspection tools
+Deliverables:
+- `list_strategies`
+- `get_strategy`
+- `get_runtime_state`
+- `get_recent_changes`
+- `get_decision_history`
+- `get_execution_state`
+
+Acceptance criteria:
+- Hermes can inspect state without knowing internals
+- dashboard and API show the same truth
+- every response is structured and auditable
+
+### Phase 3, add control tools for Hermes
+Deliverables:
+- `apply_control_decision`
+- `switch_strategy`
+- `set_risk_mode`
+- `pause_strategy`
+- `resume_strategy`
+- `reconcile_state`
+
+Acceptance criteria:
+- Hermes can control Trader through explicit verbs
+- invalid or unsafe requests are rejected cleanly
+- every transition records a reason and decision id
+
+### Phase 4, implement switching safety
+Deliverables:
+- confidence threshold
+- minimum hold time
+- hysteresis
+- explicit emergency override path
+- degraded execution fallback
+
+Acceptance criteria:
+- strategies do not flap on minor noise
+- switching reasons are deterministic and testable
+- `defensive` and `idle` are valid safe exits
+
+### Phase 5, implement initial strategies
+Deliverables:
+- `idle`
+- `defensive`
+- `trend_following`
+- `mean_reversion`
+
+Acceptance criteria:
+- each one has metadata and tests
+- each one returns clear state/explanation data
+- each one can be selected by Hermes-compatible contract shapes
+
+### Phase 6, add structure strategies
+Deliverables:
+- `grid`
+- `breakout`
+
+Acceptance criteria:
+- grid is clearly structure-based, not signal-based
+- breakout is separated from trend-following
+- tests cover the distinction
+
+### Phase 7, add event-aware behavior
+Deliverables:
+- `event_driven`
+- event-risk metadata
+- event-pressure aware selection logic
+
+Acceptance criteria:
+- Hermes can downrank normal strategies during event risk
+- event handling remains temporary and explicit
+
+### Phase 8, feed back execution quality
+Deliverables:
+- fills
+- slippage
+- latency
+- rejections
+- stress
+- degraded connectivity
+
+Acceptance criteria:
+- execution quality affects strategy selection
+- Trader surfaces the feedback clearly
+- Hermes can see why a strategy was penalized
+
+### Phase 9, tighten dashboards and audit trails
+Deliverables:
+- current strategy view
+- why-active view
+- recent changes timeline
+- Hermes decision trace
+- execution state panel
+
+Acceptance criteria:
+- a human can understand the system without reading code
+- transitions are explainable from the UI
+
+## Tests to add
+
+### Contract tests
+- strategy metadata validation
+- Hermes request schema validation
+- Trader response schema validation
+- control verb validation
+
+### Runtime tests
+- load/unload/switch transitions
+- state snapshot and restore
+- minimum hold enforcement
+- hysteresis enforcement
+- defensive fallback on degraded execution
+
+### Strategy tests
+- expected regime matching
+- avoided regime rejection
+- deterministic state transitions
+- readable explanation output
+
+### Integration tests
+- Hermes decision to Trader control path
+- Trader response parsing
+- execution feedback influencing future selection
+
+## Suggested first implementation order
+
+1. lock the contract paper
+2. canonicalize the strategy registry
+3. add metadata to current strategies
+4. add validation tests
+5. implement the inspection tools
+6. implement the control tools
+7. add switching safety
+8. implement `idle` and `defensive`
+9. implement `trend_following` and `mean_reversion`
+10. add `grid`, `breakout`, and `event_driven`
+11. wire execution feedback into selection
+12. polish dashboard and traceability
+
+## Notes on ownership and scope
+
+Trader should not become a second Hermes.
+
+If a feature answers "what should the world do?", it belongs to Hermes.
+If a feature answers "how do we run the chosen strategy safely?", it belongs to Trader.
+If a feature answers "what happened and why?", it belongs to both through a shared contract.
+
+## Output philosophy
+
+The end state should make Trader useful to Hermes, useful to humans, and still simple enough to debug.

+ 244 - 0
Hermes_Trader_Contract_v0.1.md

@@ -0,0 +1,244 @@
+# Hermes ⇄ Trader Strategy Contract (v0.1)
+
+This document defines what Hermes needs from Trader-managed strategies in order to make selection decisions.
+
+## Scope
+
+- Trader runs the strategies.
+- Hermes selects which configured strategy is active for each account and market.
+- A strategy is bound to a specific account and market.
+- Hermes queries strategies to see the available options for that account-market pair.
+- Trader may turn strategies on or off when instructed by Hermes.
+- Trader does not decide the global strategy choice on its own.
+
+## What a strategy represents
+
+A strategy is an account-market specific operating mode.
+It is not a generic idea detached from venue, balance, or market context.
+
+## Required strategy bindings
+
+Each configured strategy must be tied to:
+- `strategy_id`
+- `account_id`
+- `market`
+- `base_currency`
+- `quote_currency`
+- `enabled_state`
+
+Hermes should be able to ask, for a given account and market, which strategies exist and whether they are currently enabled.
+
+## What Hermes needs from a strategy
+
+When Hermes queries a strategy, it must receive the information below.
+
+### Identity and binding
+- `strategy_id`
+- `strategy_name`
+- `account_id`
+- `market`
+- `base_currency`
+- `quote_currency`
+- `enabled_state`
+- `mode`
+
+### Balance and exposure
+- `account_balance`
+- `available_balance`
+- `base_position`
+- `quote_position`
+- `open_orders`
+- `reserved_balance`
+- `exposure`
+- `free_margin` where relevant
+
+### Market suitability
+- `expected_market_regime`
+- `avoided_market_regime`
+- `risk_profile`
+- `liquidity_expectation`
+- `event_risk_expectation`
+- `volatility_expectation`
+
+### Live operating state
+- `status`
+- `recent_changes`
+- `decision_history`
+- `current_intent`
+- `last_transition`
+- `last_update`
+- `uncertainty`
+- `confidence`
+
+### Execution feedback
+- `fills`
+- `slippage`
+- `latency`
+- `rejections`
+- `execution_quality`
+- `stress`
+- `venue_health`
+
+### Strategy explanation
+- `current_reason`
+- `key_drivers`
+- `warnings`
+- `constraints`
+
+## Hermes decision inputs
+
+Hermes should decide using the returned strategy snapshot plus external market information.
+The minimum decision inputs are:
+- market regime
+- liquidity state
+- event risk
+- sentiment pressure
+- current strategy state
+- balance and exposure state
+- execution quality
+- confidence and uncertainty
+- recent changes
+
+## Strategy response shape
+
+A strategy query should return a compact structured snapshot with nested sections. Only information that changes Hermes decisions should be included.
+
+Example:
+
+```json
+{
+  "identity": {
+    "strategy_id": "btc-vienna-trend-1",
+    "strategy_name": "trend_following",
+    "account_id": "acc-01",
+    "market": "BTC/USD",
+    "base_currency": "BTC",
+    "quote_currency": "USD"
+  },
+  "control": {
+    "enabled_state": "on",
+    "mode": "active"
+  },
+  "fit": {
+    "expected_market_regime": "trend",
+    "avoided_market_regime": "range",
+    "risk_profile": "medium",
+    "liquidity_expectation": "normal",
+    "event_risk_expectation": "low",
+    "volatility_expectation": "moderate"
+  },
+  "position": {
+    "account_balance": {
+      "base": 0.42,
+      "quote": 18500.0
+    },
+    "available_balance": {
+      "base": 0.12,
+      "quote": 9400.0
+    },
+    "base_position": 0.30,
+    "quote_position": 0.0,
+    "reserved_balance": 1200.0,
+    "open_orders": 2,
+    "exposure": "long",
+    "free_margin": 8200.0
+  },
+  "state": {
+    "status": "running",
+    "recent_changes": ["scaled in", "trimmed exposure"],
+    "decision_history": ["held", "added", "held"],
+    "current_intent": "follow upward structure",
+    "last_transition": "2026-04-15T18:10:00Z",
+    "last_update": "2026-04-15T18:25:00Z"
+  },
+  "assessment": {
+    "confidence": 0.78,
+    "uncertainty": "low",
+    "current_reason": "trend intact, balance sufficient, execution stable",
+    "key_drivers": ["trend", "balance", "liquidity"],
+    "warnings": ["minimum_hold_minutes=15"]
+  },
+  "execution": {
+    "fills": 12,
+    "slippage": 0.0012,
+    "latency": "normal",
+    "rejections": 0,
+    "execution_quality": "good",
+    "stress": "low",
+    "venue_health": "good"
+  }
+}
+```
+
+## Strategy modes
+
+A strategy may expose modes, but modes are not separate strategies.
+Examples:
+- slow trend
+- strong trend
+- cautious mean reversion
+- high-volatility breakout
+
+## Enabled and disabled state
+
+Hermes must be able to see whether a strategy is:
+- `on`
+- `off`
+- `paused`
+- `error`
+- `observe`
+- `active`
+
+Trader may manage those states internally, but Hermes needs a clear readback of them.
+
+## Control responsibility
+
+Hermes may instruct Trader to:
+- switch a strategy on
+- switch a strategy off
+- keep a strategy on
+- keep a strategy off
+- shift risk mode
+- pause a strategy
+- resume a strategy
+
+Trader should not invent a strategy decision that conflicts with Hermes without an explicit safety reason.
+
+## Selection rules
+
+Hermes selects strategies per account-market combination using:
+- regime fit
+- balance and exposure fit
+- execution quality
+- current confidence
+- current uncertainty
+- recent state changes
+- event pressure
+- liquidity quality
+
+## Strategy taxonomy
+
+Keep the strategy set small and orthogonal:
+- `idle`
+- `defensive`
+- `trend_following`
+- `mean_reversion`
+- `grid`
+- `breakout`
+- `event_driven`
+
+## First implementation order
+
+1. Make the query result shape exact.
+2. Make balance and exposure mandatory in strategy snapshots.
+3. Make strategy bindings explicit per account and market.
+4. Keep Hermes control limited to on/off and risk posture.
+5. Implement `idle` and `defensive` first.
+6. Implement `trend_following` and `mean_reversion` next.
+7. Add `grid`, `breakout`, and `event_driven` after the core set is stable.
+8. Add tests for balance reporting, selection inputs, and enable/disable control.
+
+## Versioning
+
+This is v0.1.
+Future changes should stay focused on the information Hermes needs to make better decisions, not on exposing implementation detail.

+ 16 - 57
MCP_SURFACE_PROPOSAL.md

@@ -2,77 +2,36 @@
 
 Keep the public MCP surface small and standardized.
 
-## Proposed tools
+## Current tools
 
 ### 1. `list_strategies()`
-Returns a compact inventory of loaded strategies.
-
-```json
-{
-  "strategies": [
-    {
-      "id": "grid-1",
-      "name": "Grid Trader",
-      "mode": "active",
-      "status": "running"
-    }
-  ]
-}
-```
+Returns a compact inventory of strategies.
 
 ### 2. `get_strategy(id)`
-Returns the full strategy record, including config, state, and key live metadata.
-
+Returns compact identity/control data plus `report` by default.
 Optional flags:
+- `include_config`
+- `include_state`
 - `include_render`
 - `include_debug`
-
-```json
-{
-  "id": "grid-1",
-  "name": "Grid Trader",
-  "mode": "active",
-  "config": {
-    "grid_levels": 6,
-    "fee_rate": 0.0025
-  },
-  "state": {
-    "center_price": 2375.2,
-    "last_price": 2374.8,
-    "last_side": "buy",
-    "open_order_count": 4,
-    "last_error": ""
-  },
-  "account_id": "qndd8o9ppop6",
-  "market_symbol": "xrpusd"
-}
-```
+- `include_report` (defaults to true)
 
 ### 3. `update_strategy(id, config?, state?)`
 Writes config/state changes for an existing strategy, then reconciles it.
 
-```json
-{
-  "id": "grid-1",
-  "updated": true
-}
-```
-
 ### 4. `control_strategy(id, action)`
 Single control entry point for `start`, `pause`, `resume`, `stop`, and `reconcile`.
 
-```json
-{
-  "id": "grid-1",
-  "action": "pause",
-  "ok": true
-}
-```
+### 5. `set_strategy_policy(id, policy)`
+Stores Hermes policy metadata on the strategy.
+
+### 6. `get_capabilities()`
+Describes the current surface and the strategy record shape.
 
 ## Design notes
 
-- `get_strategy()` is the main read tool.
-- `update_strategy()` is the write tool for config/state.
-- `control_strategy()` handles lifecycle and reconcile.
-- Keep the surface compact and predictable.
-- Prefer returning real live metadata in `get_strategy()` instead of adding more specialized tools.
+- `report().fit` is the Hermes-facing fit block.
+- `set_strategy_policy()` stores high-level intent, not trading mechanics.
+- policy is reapplied on reconcile and instance creation.
+- `get_strategy()` should stay compact unless expanded explicitly.
+- keep the surface small and predictable.

+ 18 - 12
PROJECT.md

@@ -1,35 +1,41 @@
 # Trader MCP - Project
 
 ## Purpose
-Trading helpers, strategy control, and a small MCP surface. Keep app logic isolated and the public API compact.
+Trader MCP runs strategies, persists strategy state, and keeps the public surface small enough for Hermes and operators to use safely.
 
 ## Architecture
 - FastAPI app
 - FastMCP mounted at `/mcp` using SSE at `/mcp/sse`
-- App-specific logic under `src/<app_name>/*`
-- State persistence via SQLite (add only as needed)
-- Logs and pid files under `./logs/`
+- app logic under `src/trader_mcp/*`
+- strategies under `strategies/*`
+- state persistence via SQLite
+- logs under `./logs/`
 
 ## MCP surface
 - `list_strategies`
 - `get_strategy`
 - `update_strategy`
 - `control_strategy`
+- `set_strategy_policy`
+- `get_capabilities`
 
 ## Notes
-- `get_strategy()` can optionally include render and debug data.
-- `update_strategy()` updates config/state and reconciles.
+- `get_strategy()` returns compact identity/control data plus `report` by default.
+- `report().fit` is the Hermes-facing fit block.
+- `set_strategy_policy()` stores `risk_posture`, `priority`, and optional metadata.
+- policy is reapplied on reconcile and instance creation.
 - `control_strategy()` handles `start`, `pause`, `resume`, `stop`, and `reconcile`.
 
 ## Upgrade plans
 
-The next phase should focus on making trader easier to operate, not just more capable:
+The next phase should focus on Hermes compatibility and operational clarity:
 
-- standardize a shared config vocabulary across strategies
-- streamline how strategies appear in the dashboard and how they are configured
-- add more render widgets where they improve clarity, such as candle and line charts
-- keep the debug log useful, but visually lighter when possible
-- extend the strategy set only once the shared config and display model are clean
+- keep the strategy taxonomy small and orthogonal
+- make `report()` the canonical Hermes read path
+- keep policy meta-level, not trading-mechanics level
+- map policy to strategy-local parameters inside each strategy
+- reduce dashboard clutter and keep render panels useful
+- add new strategies only when the contract stays stable
 
 ## Routes
 - `GET /` minimal landing page

+ 38 - 37
README.md

@@ -1,40 +1,44 @@
-# Trader MCP v0.5.0
+# Trader MCP
 
-MCP server for trading-related helper functions, with a dashboard for accounts and strategies.
+Trader MCP runs strategies, persists their state, and exposes a compact MCP surface for operator and Hermes control.
 
-## Release notes
+## Current shape
 
-This release marks the point where the stack is genuinely usable:
-
-- grid trader places, replaces, recenters, and resizes orders as expected
-- strategy execution is stable enough to run continuously
-- shared execution now uses real fees for sizing
-- strategy state and dashboard behaviour are aligned with live trading flow
-- the public MCP surface stays compact while the runtime behaviour is much more complete
+- strategies are bound to an account and market
+- strategy snapshots expose `report()` for Hermes
+- Hermes policy is stored separately and mapped into strategy-specific parameters
+- market regime is read from `crypto-mcp`, not Trader-owned data
 
 ## What works now
 
-- grid trading with live order placement and reconciliation
-- observe, pause, resume, stop, and reconcile control actions
-- live render panels and inline strategy config editing
-- account overview in the dashboard, kept intentionally out of the way
-
-## Next ideas
+- strategy lifecycle control (`start`, `pause`, `resume`, `stop`, `reconcile`)
+- strategy snapshots with `report()` and `fit`
+- Hermes policy storage via `set_strategy_policy`
+- grid trading
+- exposure protection
+- trend following
+- dashboard inspection and inline config editing
 
-Further improvements may include more strategies, a cleaner shared config schema, and extra render widgets such as candles or line graphs. These are useful follow-ups, but they are not blockers for the current release.
-
-## Current MCP tools
+## Public MCP tools
 - `list_strategies`
 - `get_strategy`
 - `update_strategy`
 - `control_strategy`
+- `set_strategy_policy`
+- `get_capabilities`
 
-## Endpoints
-- `GET /` - landing page
-- `GET /health` - lightweight health check
-- `GET /mcp/sse` - MCP SSE transport endpoint
+## `get_strategy()` defaults
+
+By default, `get_strategy()` returns compact identity/control data plus `report`.
+Optional flags:
+- `include_config`
+- `include_state`
+- `include_render`
+- `include_debug`
+- `include_report` (defaults to true)
 
 ## Quick start
+
 ```bash
 source .venv/bin/activate
 pip install -r requirements.txt
@@ -43,20 +47,17 @@ pip install -r requirements.txt
 
 Default port: `8570`
 
-## MCP
-The public MCP surface is intentionally small:
-
-- `list_strategies()` returns a compact inventory
-- `get_strategy(id, include_render=False, include_debug=False)` returns one strategy with live metadata
-- `update_strategy(id, config=None, state=None)` updates config/state and reconciles
-- `control_strategy(id, action)` handles `start`, `pause`, `resume`, `stop`, `reconcile`
+## Endpoints
+- `GET /` landing page
+- `GET /health` health check
+- `GET /mcp/sse` MCP SSE transport endpoint
+- `GET /dashboard/` dashboard
 
-## Dashboard
-- accounts section is collapsed by default
-- strategies table stays visible
-- per-strategy details expand below the row
-- live render panels update automatically
-- config is editable inline in the detail row
+## Notes
+- `control_strategy()` handles lifecycle verbs.
+- `set_strategy_policy()` stores Hermes intent as `risk_posture` and `priority`.
+- `report().fit` is the main Hermes-facing strategy fit block.
+- policy is applied again on reconcile.
 
 ## Development
-See `run.sh`, `tests.sh`, `killserver.sh`, and `restart.sh` in this folder.
+See `run.sh`, `tests.sh`, `killserver.sh`, and `restart.sh`.

+ 10 - 3
Strategy_Contract.md

@@ -5,6 +5,7 @@ This is the canonical contract for `trader-mcp` strategies.
 ## Purpose
 
 A strategy defines behavior. It does not own persistence, lifecycle, or UI rendering.
+It must also expose a Hermes-facing `report()` snapshot with only decision-relevant data.
 
 ## Strategy class
 
@@ -40,8 +41,8 @@ class Strategy:
 
 - `CONFIG_SCHEMA` is declarative metadata
 - config is supplied by the engine
-- config changes are handled by reload
-- config should stay read-only from the strategy perspective
+- config changes are handled by reload or reconcile
+- config should stay read-only from the strategy perspective, except for derived policy application inside the strategy
 
 ## State
 
@@ -50,6 +51,7 @@ class Strategy:
 - keep state serializable where practical
 - do not depend on hidden global state
 - the engine snapshots and restores state, not the strategy
+- the engine may reapply stored policy on reconcile
 
 ## Lifecycle
 
@@ -64,7 +66,9 @@ reload -> state restored from engine snapshot if available
 The context is capability-only.
 
 Allowed:
-- market/data access
+- account data access
+- balances and open orders
+- price and market-data access
 - order placement
 - logging
 - engine-mediated actions
@@ -75,6 +79,9 @@ Not allowed:
 - config storage
 - lifecycle decisions
 - strategy identity ownership
+- market-regime ownership
+
+Market regime is a data-layer artifact provided by `crypto-mcp`, not a Trader-owned query result.
 
 ## UI output
 

+ 3 - 0
Strategy_Runtime.md

@@ -27,6 +27,7 @@ These should stay stable and audit-friendly.
 - `mode` (`off`, `observe`, `active`)
 - `config`
 - runtime state snapshots
+- stored Hermes policy metadata
 
 ## State persistence
 
@@ -65,6 +66,7 @@ Default behavior:
 - unload old runtime
 - restore state snapshot when available
 - load new runtime
+- reapply stored policy after instance creation
 
 Selective state carryover is acceptable only through engine-managed persistence.
 
@@ -76,5 +78,6 @@ The dashboard shows:
 - mode
 - runtime status
 - rendered widgets
+- policy metadata when present
 
 It does not own trading logic.

+ 240 - 30
TODO.md

@@ -1,19 +1,59 @@
-# Trader-MCP TODO
+# Trader-MCP Hermes Compatibility TODO
 
-Trader-MCP concerns only strategy behavior and the surfaces strategies must expose.
+This file is the structured upgrade plan for preparing `trader-mcp` for `hermes-mcp` supervision.
 
-## Strategy taxonomy
-- Reduce strategy set to a small, orthogonal registry.
-- Treat parameter variants as modes inside a strategy, not separate strategies.
-- Keep at minimum: `idle`, `trend_following`, `mean_reversion`, `breakout`, `grid`, `event_driven`, `defensive`.
+See also:
+- `Hermes_Trader_Contract_v0.1.md`
+- `Hermes_Compatibility_Plan.md`
+- `Strategy_Runtime.md`
+- `Strategy_Contract.md`
+- `../hermes-mcp/hermes.md`
+- `../hermes-mcp/hermes_and_strategies.md`
+
+## Guiding rule
+
+Trader-MCP concerns strategy behavior and the surfaces strategies must expose.
+Hermes decides the market story and the active control stance.
+
+## 1. Contract first
+- Freeze the Hermes ⇄ Trader contract before adding new strategy behavior.
+- Make all control verbs explicit and schema-validated.
+- Keep every request and response machine readable.
+- Require `decision_id`, `reason`, and `confidence` for Hermes control decisions.
+- Require Trader to return a structured applied-result record.
+
+## 2. Canonical strategy taxonomy
+Reduce the strategy set to a small, orthogonal registry.
+Treat parameter variants as modes inside a strategy, not separate strategies.
+
+Canonical set:
+- `idle`
+- `defensive`
+- `trend_following`
+- `mean_reversion`
+- `grid`
+- `breakout`
+- `event_driven`
+
+Rules:
 - Remove or fold redundant strategy names that differ only by tuning.
+- Keep names human-readable and stable.
+- Preserve a single conceptual strategy per regime.
+
+## 3. Strategy metadata
+Every strategy must declare:
+- `expects`
+- `avoids`
+- `risk_profile`
+- `capabilities`
 
-## Strategy metadata
-- Add explicit `expects` / `avoids` regime metadata to every strategy.
-- Declare natural habitat fields for trend, volatility, event risk, and liquidity.
-- Add a risk profile per strategy.
+The natural habitat fields should cover:
+- trend
+- volatility
+- event risk
+- liquidity
 
-### Example: strategy capability declaration
+Example capability declaration:
 ```json
 {
   "name": "mean_reversion",
@@ -27,33 +67,203 @@ Trader-MCP concerns only strategy behavior and the surfaces strategies must expo
   "avoids": {
     "trend": "strong",
     "volatility": "expanding",
-    "event_risk": "high"
+    "event_risk": "high",
+    "liquidity": "thin"
   },
   "risk_profile": "medium"
 }
 ```
 
-### Example: strategy contract to Hermes
-```json
-{
-  "name": "trend_following",
-  "inputs": ["market_regime", "sentiment_pressure", "liquidity_state"],
-  "outputs": ["target_position", "risk_mode", "reason"],
-  "exposes": ["status", "recent_changes", "decision_history"],
-  "confidence_threshold": 0.65,
-  "minimum_hold_minutes": 15
-}
-```
+## 4. Strategy surface
+Expose clean strategy state.
+Each strategy should provide:
+- `status`
+- `recent_changes`
+- `decision_history`
+- `render_state`
+- `risk_state`
+- `execution_state`
+
+Preferred additions:
+- `confidence`
+- `uncertainty`
+- `last_transition`
+- `last_error`
+
+## 5. Trader surface for Hermes
+Refactor the MCP tools toward Hermes usefulness.
+
+Priority read tools:
+- `list_strategies`
+- `get_strategy`
+- `get_runtime_state`
+- `get_recent_changes`
+- `get_decision_history`
+- `get_execution_state`
+
+Priority control tools:
+- `apply_control_decision`
+- `switch_strategy`
+- `set_risk_mode`
+- `pause_strategy`
+- `resume_strategy`
+- `reconcile_state`
+
+## 6. Switching policy
+Prevent flip-flopping.
+
+Add:
+- confidence threshold
+- minimum hold time
+- hysteresis
+- explicit emergency override path
+- degraded execution fallback
+
+## 7. Ownership boundaries
+
+### Strategy owns
+- local logic
+- local state
+- local config
+- own render output
+- own explanation string
 
-## Strategy surface
-- Expose clean strategy state.
-- Provide strategy status, recent changes, and decision history.
+### Trader runtime owns
+- lifecycle
+- persistence
+- snapshots
+- reconciliation
+- dashboard presentation
+- execution plumbing
 
-## Execution feedback
-- Feed fills, slippage, execution quality, and stress into strategy selection.
-- Let execution degradation influence strategy selection.
+### Human observer owns
+- inspection
+- approval where configured
+- operational overrides where explicitly allowed
+- debugging and review
 
-## Cleanup / simplification
+### Hermes owns
+- enable / disable / switch decisions
+- risk-mode changes
+- uncertainty and confidence weighting
+- regime interpretation
+- execution-degradation-driven downranking
+
+## 8. First strategies to implement
+Implement in this order:
+1. `idle`
+2. `defensive`
+3. `trend_following`
+4. `mean_reversion`
+5. `grid`
+6. `breakout`
+7. `event_driven`
+
+Notes:
+- `idle` and `defensive` are safety exits.
+- `trend_following` and `mean_reversion` are the first core alpha strategies.
+- `grid` is structure-based, not signal-based.
+- `breakout` is separate from trend-following.
+- `event_driven` should remain explicit and temporary in behavior.
+
+## 9. Stepwise upgrade path toward Hermes compatibility
+### Phase 0, document the contract
+- write `Hermes_Trader_Contract_v0.1.md`
+- agree the control verbs
+- agree the request/response schemas
+
+### Phase 1, normalize the registry
+- fold redundant strategy names
+- mark parameter variants as modes
+- add metadata to current strategies
+
+### Phase 2, expose Hermes-facing inspection tools
+- add the read tools listed above
+- ensure dashboard and API agree
+
+### Phase 3, add control tools
+- add the control tools listed above
+- validate every transition
+- log the reason for every decision
+
+### Phase 4, implement switching safety
+- confidence threshold
+- hold time
+- hysteresis
+- emergency override
+- degraded execution fallback
+
+### Phase 5, implement the initial strategy set
+- `idle`
+- `defensive`
+- `trend_following`
+- `mean_reversion`
+
+### Phase 6, add structure strategies
+- `grid`
+- `breakout`
+
+### Phase 7, add event-aware behavior
+- `event_driven`
+- event-risk metadata
+- event-pressure aware selection
+
+### Phase 8, feed back execution quality
+- fills
+- slippage
+- latency
+- rejected orders
+- venue stress
+- partial fill patterns
+- degraded connectivity
+
+### Phase 9, tighten dashboards and audit trails
+- current strategy view
+- why-active view
+- recent changes timeline
+- Hermes decision trace
+- execution state panel
+
+## 10. Tests to add
+### Contract tests
+- strategy metadata validation
+- Hermes request schema validation
+- Trader response schema validation
+- control verb validation
+
+### Runtime tests
+- load / unload / switch transitions
+- state snapshot and restore
+- minimum hold enforcement
+- hysteresis enforcement
+- defensive fallback on degraded execution
+
+### Strategy tests
+- expected regime matching
+- avoided regime rejection
+- deterministic state transitions
+- readable explanation output
+
+### Integration tests
+- Hermes decision to Trader control path
+- Trader response parsing
+- execution feedback influencing future selection
+
+## 11. Cleanup and simplification
 - Separate signal-based strategies from structure-based ones, especially `mean_reversion` vs `grid`.
 - Avoid adding micro-strategies until the minimal taxonomy is stable.
 - Keep strategy names human-readable and stable.
+
+## 12. Current implementation order
+1. lock the contract paper
+2. canonicalize the strategy registry
+3. add metadata to current strategies
+4. add validation tests
+5. implement the inspection tools
+6. implement the control tools
+7. add switching safety
+8. implement `idle` and `defensive`
+9. implement `trend_following` and `mean_reversion`
+10. add `grid`, `breakout`, and `event_driven`
+11. wire execution feedback into selection
+12. polish dashboard and traceability

+ 72 - 12
src/trader_mcp/server.py

@@ -132,11 +132,18 @@ def list_strategies() -> dict:
     return {"strategies": strategies}
 
 
-def get_strategy(instance_id: str, include_render: bool = False, include_debug: bool = False) -> dict:
+def get_strategy(
+    instance_id: str,
+    include_config: bool = False,
+    include_state: bool = False,
+    include_render: bool = False,
+    include_debug: bool = False,
+    include_report: bool = True,
+) -> dict:
     """get_strategy(instance_id)
 
-    Return one strategy record with config, state, and live runtime metadata.
-    Optional render and debug data can be included on demand.
+    Return one strategy record with compact live metadata.
+    Expanded config, state, render, and debug data are opt-in. Report is included by default.
     """
     record = next((r for r in list_strategy_instances() if r.id == instance_id), None)
     if record is None:
@@ -160,7 +167,20 @@ def get_strategy(instance_id: str, include_render: bool = False, include_debug:
     if include_debug:
         debug = state.get("debug_log") or []
 
-    return {
+    report = None
+    if include_report:
+        try:
+            instance = runtime.instance if runtime is not None else None
+            if instance is None:
+                from .strategy_engine import _instantiate
+
+                instance = _instantiate(record)
+                instance.state = state
+            report = instance.report()
+        except Exception as exc:
+            report = {"error": str(exc)}
+
+    response = {
         "ok": True,
         "id": record.id,
         "name": record.name or record.strategy_type,
@@ -172,15 +192,22 @@ def get_strategy(instance_id: str, include_render: bool = False, include_debug:
         "market_symbol": record.market_symbol,
         "base_currency": record.base_currency,
         "counter_currency": record.counter_currency,
-        "config": record.config,
-        "state": state,
-        "last_price": state.get("last_price"),
-        "last_side": state.get("last_side") or state.get("last_action"),
-        "open_order_count": state.get("open_order_count", 0),
-        "last_error": state.get("last_error", ""),
-        "render": render,
-        "debug_log": debug,
     }
+    if include_config:
+        response["config"] = record.config
+    if include_state:
+        response["state"] = state
+        response["last_price"] = state.get("last_price")
+        response["last_side"] = state.get("last_side") or state.get("last_action")
+        response["open_order_count"] = state.get("open_order_count", 0)
+        response["last_error"] = state.get("last_error", "")
+    if include_report:
+        response["report"] = report
+    if include_render:
+        response["render"] = render
+    if include_debug:
+        response["debug_log"] = debug
+    return response
 
 
 def update_strategy(instance_id: str, config: dict | None = None, state: dict | None = None) -> dict:
@@ -199,6 +226,33 @@ def update_strategy(instance_id: str, config: dict | None = None, state: dict |
     return {"ok": False, "id": instance_id}
 
 
+def set_strategy_policy(instance_id: str, policy: dict) -> dict:
+    """set_strategy_policy(instance_id, policy)
+
+    Store a high-level Hermes policy on the strategy and persist it.
+    Policy is intentionally abstract, for example: risk_posture and priority.
+    """
+    record = next((r for r in list_strategy_instances() if r.id == instance_id), None)
+    if record is None:
+        return {"ok": False, "id": instance_id, "error": "strategy not found"}
+    if not isinstance(policy, dict):
+        return {"ok": False, "id": instance_id, "error": "policy must be an object"}
+    config = dict(record.config or {})
+    config["policy"] = {
+        "risk_posture": policy.get("risk_posture", config.get("policy", {}).get("risk_posture", "normal")),
+        "priority": policy.get("priority", config.get("policy", {}).get("priority", "normal")),
+        "reason": policy.get("reason", config.get("policy", {}).get("reason", "")),
+        "decision_id": policy.get("decision_id", config.get("policy", {}).get("decision_id", "")),
+    }
+    state = dict(record.state or {})
+    state["policy"] = config["policy"]
+    ok_config = update_strategy_config(instance_id, config)
+    ok_state = update_strategy_state(instance_id, state)
+    if ok_config and ok_state:
+        return reconcile_instance(instance_id)
+    return {"ok": False, "id": instance_id, "error": "failed to persist policy"}
+
+
 def control_strategy(instance_id: str, action: str) -> dict:
     """control_strategy(instance_id, action)
 
@@ -247,6 +301,11 @@ def get_capabilities() -> dict:
                 "description": "Control lifecycle or reconcile with a single action.",
                 "params": {"action": "start|pause|resume|stop|reconcile"},
             },
+            {
+                "name": "set_strategy_policy",
+                "description": "Store a high-level Hermes policy on a strategy.",
+                "params": {"policy": "{risk_posture, priority, reason?, decision_id?}"},
+            },
         ],
         "strategy_summary_fields": [
             "id",
@@ -283,6 +342,7 @@ if FastMCP is not None:
     mcp.tool()(get_strategy)
     mcp.tool()(update_strategy)
     mcp.tool()(control_strategy)
+    mcp.tool()(set_strategy_policy)
     mcp.tool()(get_capabilities)
 
     app.mount("/mcp", mcp.sse_app())

+ 37 - 0
src/trader_mcp/strategy_context.py

@@ -56,6 +56,43 @@ class StrategyContext:
     def get_account_info(self) -> Any:
         return get_account_info(self.account_id)
 
+    def get_balance_snapshot(self) -> dict[str, Any]:
+        info = self.get_account_info()
+        balances = info.get("balances") if isinstance(info, dict) else []
+        if not isinstance(balances, list):
+            balances = []
+        return {
+            "account_id": self.account_id,
+            "market_symbol": self.market_symbol,
+            "base_currency": self.base_currency,
+            "counter_currency": self.counter_currency,
+            "balances": balances,
+        }
+
+    def get_open_order_snapshot(self) -> dict[str, Any]:
+        orders = self.get_open_orders()
+        return {
+            "account_id": self.account_id,
+            "market_symbol": self.market_symbol,
+            "open_orders": orders if isinstance(orders, list) else [],
+        }
+
+    def get_strategy_snapshot(self) -> dict[str, Any]:
+        return {
+            "identity": {
+                "strategy_id": self.id,
+                "account_id": self.account_id,
+                "market": self.market_symbol,
+                "base_currency": self.base_currency,
+                "quote_currency": self.counter_currency,
+            },
+            "control": {
+                "mode": getattr(self, "mode", "off"),
+            },
+            "position": self.get_balance_snapshot(),
+            "orders": self.get_open_order_snapshot(),
+        }
+
     def _available_balance(self, asset_code: str) -> float:
         try:
             info = self.get_account_info()

+ 6 - 0
src/trader_mcp/strategy_engine.py

@@ -171,6 +171,12 @@ def _instantiate(record: StrategyRecord) -> Strategy:
     )
     instance = strategy_cls(context=context, config=record.config)
     instance.state = record.state or instance.init()
+    apply_policy = getattr(instance, "apply_policy", None)
+    if callable(apply_policy):
+        try:
+            apply_policy()
+        except Exception:
+            pass
     return instance
 
 

+ 75 - 0
src/trader_mcp/strategy_sdk.py

@@ -8,6 +8,15 @@ class Strategy:
     CONFIG_SCHEMA: dict[str, Any] = {}
     # STATE_SCHEMA declares which instance-local state should be persisted by the engine.
     STATE_SCHEMA: dict[str, Any] = {}
+    # REPORT_SCHEMA describes the structured Hermes-facing snapshot returned by report().
+    REPORT_SCHEMA: dict[str, Any] = {}
+    # STRATEGY_PROFILE describes when Hermes should prefer this strategy.
+    STRATEGY_PROFILE: dict[str, Any] = {
+        "expects": {},
+        "avoids": {},
+        "risk_profile": "unknown",
+        "capabilities": [],
+    }
     TICK_MINUTES: float = 1.0
 
     def __init__(self, context, config):
@@ -24,5 +33,71 @@ class Strategy:
     def on_stop(self):
         return None
 
+    def describe(self):
+        return {
+            "label": getattr(self, "LABEL", self.__class__.__name__),
+            "strategy_type": self.__class__.__module__.rsplit(".", 1)[-1],
+            "config_schema": self.CONFIG_SCHEMA,
+            "state_schema": self.STATE_SCHEMA,
+            "report_schema": self.REPORT_SCHEMA,
+            "tick_minutes": self.TICK_MINUTES,
+        }
+
+    def apply_policy(self):
+        policy = dict(self.config.get("policy") or {})
+        self.state = dict(self.state or {})
+        self.state["applied_policy"] = policy
+        return policy
+
+    def report(self):
+        snapshot = {}
+        if hasattr(self.context, "get_strategy_snapshot"):
+            try:
+                snapshot = self.context.get_strategy_snapshot() or {}
+            except Exception:
+                snapshot = {}
+        identity = snapshot.get("identity") if isinstance(snapshot.get("identity"), dict) else {}
+        control = snapshot.get("control") if isinstance(snapshot.get("control"), dict) else {}
+        position = snapshot.get("position") if isinstance(snapshot.get("position"), dict) else {}
+        orders = snapshot.get("orders") if isinstance(snapshot.get("orders"), dict) else {}
+        return {
+            "identity": {
+                "strategy_id": identity.get("strategy_id", getattr(self.context, "id", None)),
+                "strategy_name": identity.get("strategy_name", getattr(self, "LABEL", self.__class__.__name__)),
+                "account_id": identity.get("account_id", getattr(self.context, "account_id", None)),
+                "market": identity.get("market", getattr(self.context, "market_symbol", None)),
+                "base_currency": identity.get("base_currency", getattr(self.context, "base_currency", None)),
+                "quote_currency": identity.get("quote_currency", getattr(self.context, "counter_currency", None)),
+            },
+            "control": {
+                "enabled_state": control.get("enabled_state", "on" if getattr(self.context, "mode", "off") != "off" else "off"),
+                "mode": control.get("mode", getattr(self.context, "mode", "off")),
+            },
+            "position": {
+                "balance": position.get("balances") if isinstance(position.get("balances"), dict) else position.get("balances", {}),
+                "open_orders": orders.get("open_orders") if isinstance(orders.get("open_orders"), list) else [],
+                "exposure": position.get("exposure"),
+                "reserved_balance": position.get("reserved_balance"),
+                "free_margin": position.get("free_margin"),
+            },
+            "state": dict(self.state or {}),
+            "assessment": {
+                "confidence": None,
+                "uncertainty": None,
+                "reason": None,
+                "warnings": [],
+                "policy": dict(self.config.get("policy") or {}),
+            },
+            "fit": dict(getattr(self, "STRATEGY_PROFILE", {}) or {}),
+            "execution": {
+                "fills": None,
+                "slippage": None,
+                "latency": None,
+                "rejections": None,
+                "execution_quality": None,
+                "stress": None,
+            },
+        }
+
     def render(self):
         return {"widgets": []}

+ 31 - 0
strategies/exposure_protector.md

@@ -0,0 +1,31 @@
+# Exposure Protector
+
+Defensive rebalancer that trims skew and protects exposure.
+
+## Best used when
+- one side of the book dominates the account
+- Hermes wants to reduce imbalance
+- the market is still tradable, but caution is needed
+- controlled rebalancing is more important than new alpha
+
+## Avoid when
+- the market is thin or chaotic
+- the venue is unstable
+- Hermes wants a pure entry strategy
+- frequent churn would be too expensive
+
+## Core parameters
+- `trail_distance_pct`: distance from price for execution
+- `rebalance_target_ratio`: desired base/value balance
+- `rebalance_step_ratio`: how much to move per action
+- `min_rebalance_seconds`: minimum time between actions
+- `min_price_move_pct`: minimum move before a new action
+
+## Hermes policy mapping
+- `risk_posture` controls caution vs aggression
+- `priority` controls urgency and cadence
+
+## Notes
+- This strategy is passive until Hermes enables it.
+- It should protect and rebalance, not decide regime.
+- Trader derives concrete execution values from policy.

+ 115 - 30
strategies/stop_loss_trader.py → strategies/exposure_protector.py

@@ -7,12 +7,25 @@ from src.trader_mcp.logging_utils import log_event
 
 
 class Strategy(Strategy):
-    LABEL = "Stop Loss Rebalancer"
+    LABEL = "Exposure Protector"
+    STRATEGY_PROFILE = {
+        "expects": {
+            "trend": "strong",
+            "volatility": "moderate",
+            "event_risk": "low",
+            "liquidity": "normal",
+        },
+        "avoids": {
+            "trend": "range",
+            "volatility": "chaotic",
+            "event_risk": "high",
+            "liquidity": "thin",
+        },
+        "risk_profile": "defensive",
+        "capabilities": ["exposure_trim", "trail_protection", "balance_recovery"],
+    }
     TICK_MINUTES = 0.2
     CONFIG_SCHEMA = {
-        "regime_timeframes": {"type": "list", "default": ["1d", "4h", "1h", "15m"]},
-        "trend_enter_threshold": {"type": "float", "default": 0.7, "min": 0.0, "max": 1.0},
-        "trend_exit_threshold": {"type": "float", "default": 0.45, "min": 0.0, "max": 1.0},
         "trail_distance_pct": {"type": "float", "default": 0.03, "min": 0.0, "max": 1.0},
         "rebalance_target_ratio": {"type": "float", "default": 0.5, "min": 0.0, "max": 1.0},
         "rebalance_step_ratio": {"type": "float", "default": 0.15, "min": 0.0, "max": 1.0},
@@ -20,6 +33,8 @@ class Strategy(Strategy):
         "max_order_size": {"type": "float", "default": 0.0, "min": 0.0},
         "order_spacing_ticks": {"type": "int", "default": 1, "min": 0, "max": 1000},
         "cooldown_ticks": {"type": "int", "default": 2, "min": 0, "max": 1000},
+        "min_rebalance_seconds": {"type": "int", "default": 300, "min": 0, "max": 86400},
+        "min_price_move_pct": {"type": "float", "default": 0.01, "min": 0.0, "max": 1.0},
         "balance_tolerance": {"type": "float", "default": 0.05, "min": 0.0, "max": 1.0},
         "fee_rate": {"type": "float", "default": 0.0025, "min": 0.0, "max": 0.05},
         "debug_orders": {"type": "bool", "default": True},
@@ -35,6 +50,8 @@ class Strategy(Strategy):
         "counter_available": {"type": "float", "default": 0.0},
         "trailing_anchor": {"type": "float", "default": 0.0},
         "cooldown_remaining": {"type": "int", "default": 0},
+        "last_order_at": {"type": "float", "default": 0.0},
+        "last_order_price": {"type": "float", "default": 0.0},
     }
 
     def init(self):
@@ -42,13 +59,15 @@ class Strategy(Strategy):
             "last_price": 0.0,
             "last_action": "idle",
             "last_error": "",
-            "debug_log": ["init stop loss rebalancer"],
+            "debug_log": ["init exposure protector"],
             "regimes": {},
             "regimes_updated_at": "",
             "base_available": 0.0,
             "counter_available": 0.0,
             "trailing_anchor": 0.0,
             "cooldown_remaining": 0,
+            "last_order_at": 0.0,
+            "last_order_price": 0.0,
         }
 
     def _log(self, message: str) -> None:
@@ -65,6 +84,44 @@ class Strategy(Strategy):
     def _market_symbol(self) -> str:
         return self.context.market_symbol or f"{self._base_symbol().lower()}usd"
 
+    def apply_policy(self):
+        policy = super().apply_policy()
+        risk = str(policy.get("risk_posture") or "normal").lower()
+        priority = str(policy.get("priority") or "normal").lower()
+
+        trail_map = {"cautious": 0.02, "normal": 0.03, "assertive": 0.04}
+        step_map = {"cautious": 0.08, "normal": 0.15, "assertive": 0.25}
+        wait_map = {"cautious": 600, "normal": 300, "assertive": 120}
+        move_map = {"cautious": 0.02, "normal": 0.01, "assertive": 0.005}
+
+        if priority in {"low", "background"}:
+            trail = trail_map.get("cautious", 0.02)
+            step = step_map.get("cautious", 0.08)
+            wait = wait_map.get("cautious", 600)
+            move = move_map.get("cautious", 0.02)
+        elif priority in {"high", "urgent"}:
+            trail = trail_map.get("assertive", 0.04)
+            step = step_map.get("assertive", 0.25)
+            wait = wait_map.get("assertive", 120)
+            move = move_map.get("assertive", 0.005)
+        else:
+            trail = trail_map.get(risk, 0.03)
+            step = step_map.get(risk, 0.15)
+            wait = wait_map.get(risk, 300)
+            move = move_map.get(risk, 0.01)
+
+        self.config["trail_distance_pct"] = trail
+        self.config["rebalance_step_ratio"] = step
+        self.config["min_rebalance_seconds"] = wait
+        self.config["min_price_move_pct"] = move
+        self.state["policy_derived"] = {
+            "trail_distance_pct": trail,
+            "rebalance_step_ratio": step,
+            "min_rebalance_seconds": wait,
+            "min_price_move_pct": move,
+        }
+        return policy
+
     def _live_fee_rate(self) -> float:
         try:
             payload = self.context.get_fee_rates(self._market_symbol())
@@ -78,13 +135,10 @@ class Strategy(Strategy):
         return float(payload.get("price") or 0.0)
 
     def _refresh_regimes(self) -> None:
-        regimes: dict[str, dict] = {}
-        for tf in self.config.get("regime_timeframes") or ["1d", "4h", "1h", "15m"]:
-            try:
-                regimes[str(tf)] = self.context.get_regime(self._base_symbol(), str(tf))
-            except Exception as exc:
-                regimes[str(tf)] = {"error": str(exc)}
-        self.state["regimes"] = regimes
+        try:
+            self.state["regimes"] = self.context.get_strategy_snapshot().get("fit", {}) if hasattr(self.context, "get_strategy_snapshot") else {}
+        except Exception:
+            self.state["regimes"] = {}
         self.state["regimes_updated_at"] = datetime.now(timezone.utc).isoformat()
 
     def _refresh_balance_snapshot(self) -> None:
@@ -111,19 +165,6 @@ class Strategy(Strategy):
             if asset == quote:
                 self.state["counter_available"] = available
 
-    def _regime_strength(self) -> float:
-        regimes = self.state.get("regimes") or {}
-        strengths = []
-        for tf in self.config.get("regime_timeframes") or []:
-            regime = regimes.get(str(tf)) or {}
-            trend = regime.get("trend") or {}
-            strengths.append(float(trend.get("strength") or 0.0))
-        return max(strengths) if strengths else 0.0
-
-    def _is_trending(self) -> bool:
-        strength = self._regime_strength()
-        return strength >= float(self.config.get("trend_enter_threshold", 0.7) or 0.7)
-
     def _account_value_ratio(self, price: float) -> float:
         base_value = float(self.state.get("base_available") or 0.0) * price
         counter_value = float(self.state.get("counter_available") or 0.0)
@@ -182,9 +223,20 @@ class Strategy(Strategy):
             self.state["last_action"] = "cooldown"
             return {"action": "cooldown", "price": price}
 
-        if not self._is_trending():
-            self.state["last_action"] = "standby"
-            return {"action": "standby", "price": price}
+        now = datetime.now(timezone.utc).timestamp()
+        last_order_at = float(self.state.get("last_order_at") or 0.0)
+        min_rebalance_seconds = int(self.config.get("min_rebalance_seconds", 300) or 0)
+        if last_order_at and min_rebalance_seconds > 0 and (now - last_order_at) < min_rebalance_seconds:
+            self.state["last_action"] = "hold"
+            return {"action": "hold", "price": price, "reason": "rebalance cooldown"}
+
+        last_order_price = float(self.state.get("last_order_price") or 0.0)
+        min_price_move_pct = float(self.config.get("min_price_move_pct", 0.01) or 0.0)
+        if last_order_price > 0 and min_price_move_pct > 0:
+            move_pct = abs(price - last_order_price) / last_order_price
+            if move_pct < min_price_move_pct:
+                self.state["last_action"] = "hold"
+                return {"action": "hold", "price": price, "reason": "insufficient price move", "move_pct": move_pct}
 
         side = self._desired_side(price)
         amount = self._suggest_amount(side, price)
@@ -198,10 +250,10 @@ class Strategy(Strategy):
             market = self._market_symbol()
             if side == "sell":
                 self.state["trailing_anchor"] = max(float(self.state.get("trailing_anchor") or 0.0), price)
-                order_price = round(price * (1 + trail_distance), 8)
+                order_price = round(price * (1 - trail_distance), 8)
             else:
                 self.state["trailing_anchor"] = min(float(self.state.get("trailing_anchor") or price), price) if self.state.get("trailing_anchor") else price
-                order_price = round(price * (1 - trail_distance), 8)
+                order_price = round(price * (1 + trail_distance), 8)
 
             if self.config.get("debug_orders", True):
                 self._log(f"{side} rebalance amount={amount:.6g} price={order_price} ratio={self._account_value_ratio(price):.4f}")
@@ -214,6 +266,8 @@ class Strategy(Strategy):
                 market=market,
             )
             self.state["cooldown_remaining"] = int(self.config.get("cooldown_ticks", 2) or 2)
+            self.state["last_order_at"] = now
+            self.state["last_order_price"] = order_price
             self.state["last_action"] = f"{side}_rebalance"
             return {"action": side, "price": order_price, "amount": amount, "result": result}
         except Exception as exc:
@@ -222,6 +276,37 @@ class Strategy(Strategy):
             self.state["last_action"] = "error"
             return {"action": "error", "price": price, "error": str(exc)}
 
+    def report(self):
+        snapshot = self.context.get_strategy_snapshot() if hasattr(self.context, "get_strategy_snapshot") else {}
+        return {
+            "identity": snapshot.get("identity", {}),
+            "control": snapshot.get("control", {}),
+            "fit": dict(getattr(self, "STRATEGY_PROFILE", {}) or {}),
+            "position": {
+                "balances": {
+                    "base_available": self.state.get("base_available", 0.0),
+                    "counter_available": self.state.get("counter_available", 0.0),
+                },
+                "open_orders": snapshot.get("orders", {}).get("open_orders", []),
+                "exposure": "managed",
+            },
+            "state": {
+                "last_price": self.state.get("last_price", 0.0),
+                "last_action": self.state.get("last_action", "idle"),
+                "trailing_anchor": self.state.get("trailing_anchor", 0.0),
+                "cooldown_remaining": self.state.get("cooldown_remaining", 0),
+                "regimes_updated_at": self.state.get("regimes_updated_at", ""),
+            },
+            "assessment": {
+                "confidence": None,
+                "uncertainty": None,
+                "reason": "defensive exposure protection",
+                "warnings": [],
+                "policy": dict(self.config.get("policy") or {}),
+            },
+            "execution": snapshot.get("execution", {}),
+        }
+
     def render(self):
         return {
             "widgets": [

+ 32 - 188
strategies/grid_trader.md

@@ -1,188 +1,32 @@
-# Grid Trader Configuration
-
-This file explains every config parameter used by `strategies/grid_trader.py`.
-
-## Core grid behavior
-
-### `grid_levels`
-- **Type:** int
-- **Default:** `6`
-- **Range:** `1..20`
-- **What it does:** Number of grid orders kept per side.
-- **When to change it:**
-  - Lower it for a smaller, simpler grid.
-  - Raise it if you want a wider ladder and more incremental entries/exits.
-- **Tradeoff:** More levels means more orders, more clutter, and usually slower capital usage.
-
-### `grid_step_pct`
-- **Type:** float
-- **Default:** `0.012`
-- **Range:** `0.001..0.1`
-- **What it does:** Base spacing between grid levels as a percentage of price.
-- **When to change it:**
-  - Increase it in volatile markets.
-  - Decrease it in quiet, tight ranges.
-- **Tradeoff:** Too small can make the grid noisy, too large can make it miss moves.
-
-### `volatility_timeframe`
-- **Type:** string
-- **Default:** `"1h"`
-- **What it does:** Timeframe used to measure volatility for adaptive grid sizing.
-- **When to change it:**
-  - Use a shorter timeframe for faster adaptation.
-  - Use a longer one for smoother, slower reactions.
-- **Tradeoff:** Shorter = more reactive, longer = more stable.
-
-### `volatility_multiplier`
-- **Type:** float
-- **Default:** `0.5`
-- **Range:** `0.0..10.0`
-- **What it does:** Scales ATR-based volatility into the live grid step.
-- **When to change it:**
-  - Increase it if you want the grid to widen more aggressively in volatility.
-  - Decrease it if you want tighter spacing.
-- **Tradeoff:** Higher values make the grid less sensitive to the base step.
-
-### `grid_step_min_pct`
-- **Type:** float
-- **Default:** `0.005`
-- **Range:** `0.0001..0.5`
-- **What it does:** Lower bound for the live grid step.
-- **When to change it:**
-  - Raise it if the grid gets too tight.
-  - Lower it if you want very fine spacing.
-
-### `grid_step_max_pct`
-- **Type:** float
-- **Default:** `0.03`
-- **Range:** `0.0001..1.0`
-- **What it does:** Upper bound for the live grid step.
-- **When to change it:**
-  - Lower it to prevent the grid from becoming too wide in volatile markets.
-  - Raise it if you want the strategy to tolerate bigger swings.
-
-## Position sizing and inventory
-
-### `order_size`
-- **Type:** float
-- **Default:** `0.0`
-- **What it does:** Fixed per-order size when you want manual sizing.
-- **When to change it:**
-  - Set it when you want a static size instead of wallet-derived sizing.
-  - Leave it at `0.0` to let the strategy derive size from balance logic.
-
-### `max_notional_per_order`
-- **Type:** float
-- **Default:** `0.0`
-- **What it does:** Upper limit for the notional value of a single order.
-- **When to change it:**
-  - Set it to cap order size risk.
-  - Leave it at `0.0` for no explicit per-order cap.
-- **Note:** With `dust_collect=true`, the strategy may exceed this cap to clear small leftover balances, but never more than available funds.
-
-### `dust_collect`
-- **Type:** bool
-- **Default:** `False`
-- **What it does:** Allows the sizing logic to exceed `max_notional_per_order` if needed to consume dust.
-- **When to change it:**
-  - Enable it when you want the strategy to clean up small leftover balances.
-  - Leave it off if you want strict order caps.
-
-## Re-centering
-
-### `recenter_pct`
-- **Type:** float
-- **Default:** `0.05`
-- **Range:** `0.0..0.5`
-- **What it does:** Baseline deviation threshold that can trigger a recenter.
-- **When to change it:**
-  - Lower it for faster recentering.
-  - Raise it if you want the grid to stay anchored longer.
-- **Tradeoff:** Too low can make the grid chase price.
-
-### `recenter_atr_multiplier`
-- **Type:** float
-- **Default:** `0.35`
-- **Range:** `0.0..10.0`
-- **What it does:** Adjusts the recenter threshold using volatility.
-- **When to change it:**
-  - Increase it if you want recentering to be less eager in volatile markets.
-  - Decrease it if you want it to react faster.
-
-### `recenter_min_pct`
-- **Type:** float
-- **Default:** `0.0025`
-- **Range:** `0.0..0.5`
-- **What it does:** Minimum allowed recenter threshold.
-- **When to change it:**
-  - Raise it to prevent over-reactive recentering.
-  - Lower it if you want very tight anchor control.
-
-### `recenter_max_pct`
-- **Type:** float
-- **Default:** `0.03`
-- **Range:** `0.0..0.5`
-- **What it does:** Maximum allowed recenter threshold.
-- **When to change it:**
-  - Raise it if you want the strategy to tolerate larger drifts before recentring.
-  - Lower it if you want a tighter leash.
-
-## Fees and execution
-
-### `fee_rate`
-- **Type:** float
-- **Default:** `0.0025`
-- **Range:** `0.0..0.05`
-- **What it does:** Fallback fee assumption used when live fee lookup is unavailable.
-- **When to change it:**
-  - Set it to match your venue if fee lookup is unreliable.
-  - Leave it as a fallback safety value if live fees are available.
-
-### `order_call_delay_ms`
-- **Type:** int
-- **Default:** `250`
-- **Range:** `0..10000`
-- **What it does:** Delay between order calls.
-- **When to change it:**
-  - Increase it if the exchange is sensitive to bursts.
-  - Set it to `0` for test or fast local runs.
-
-## Trend protection
-
-### `enable_trend_guard`
-- **Type:** bool
-- **Default:** `True`
-- **What it does:** Enables the higher-timeframe trend guard.
-- **When to change it:**
-  - Disable it if you want the grid to trade regardless of regime.
-  - Keep it on if you want the strategy to stand down in strong opposing trends.
-
-### `trend_guard_reversal_max`
-- **Type:** float
-- **Default:** `0.25`
-- **Range:** `0.0..1.0`
-- **What it does:** Maximum reversal score allowed before the trend guard stops the grid.
-- **When to change it:**
-  - Lower it for stricter trend filtering.
-  - Raise it if you want the grid to remain active more often.
-
-## Visibility
-
-### `debug_orders`
-- **Type:** bool
-- **Default:** `True`
-- **What it does:** Enables verbose order/debug logging.
-- **When to change it:**
-  - Keep it on while tuning.
-  - Turn it off when you want quieter logs.
-
-## Practical starting points
-
-- **Conservative:** higher `recenter_pct`
-- **Reactive:** lower `recenter_pct`, smaller `grid_step_pct`
-- **Range-friendly:** moderate `grid_levels`, moderate `grid_step_pct`, `enable_trend_guard=true`
-
-## Current recommended interpretation
-
-Rebuild now means: cancel all open orders, recenter to the current price, then place a fresh full grid.
-The only rebuild triggers are the configured out-of-range recenter and missing tracked orders.
+# Grid Trader
+
+Passive, structure-based liquidity strategy.
+
+## Best used when
+- price is range-bound or choppy
+- volatility is low to moderate
+- liquidity is decent
+- Hermes wants to harvest mean reversion inside a structure
+
+## Avoid when
+- trend is strong and persistent
+- event risk is high
+- liquidity is thin or chaotic
+- execution quality is poor
+
+## Core parameters
+- `grid_levels`: number of orders per side
+- `grid_step_pct`: spacing between levels
+- `recenter_pct`: when to rebuild the grid
+- `volatility_timeframe`: timeframe used for adaptive sizing
+- `max_notional_per_order`: per-order cap
+- `fee_rate`: fallback fee assumption
+
+## Hermes policy mapping
+- `risk_posture` adjusts grid spacing, levels, and recentering
+- `priority` adjusts aggressiveness and order cadence
+
+## Notes
+- The strategy does not decide regime fit itself.
+- Hermes decides activation.
+- Trader applies policy on reconcile.

+ 74 - 39
strategies/grid_trader.py

@@ -9,6 +9,22 @@ from src.trader_mcp.logging_utils import log_event
 
 class Strategy(Strategy):
     LABEL = "Grid Trader"
+    STRATEGY_PROFILE = {
+        "expects": {
+            "trend": "none",
+            "volatility": "low",
+            "event_risk": "low",
+            "liquidity": "normal",
+        },
+        "avoids": {
+            "trend": "strong",
+            "volatility": "expanding",
+            "event_risk": "high",
+            "liquidity": "thin",
+        },
+        "risk_profile": "medium",
+        "capabilities": ["structure_harvesting", "range_making", "liquidity_harvesting"],
+    }
     TICK_MINUTES = 0.50
     CONFIG_SCHEMA = {
         "grid_levels": {"type": "int", "default": 6, "min": 1, "max": 20},
@@ -27,8 +43,6 @@ class Strategy(Strategy):
         "max_notional_per_order": {"type": "float", "default": 0.0, "min": 0.0},
         "dust_collect": {"type": "bool", "default": False},
         "order_call_delay_ms": {"type": "int", "default": 250, "min": 0, "max": 10000},
-        "enable_trend_guard": {"type": "bool", "default": True},
-        "trend_guard_reversal_max": {"type": "float", "default": 0.25, "min": 0.0, "max": 1.0},
         "debug_orders": {"type": "bool", "default": True},
     }
     STATE_SCHEMA = {
@@ -42,7 +56,6 @@ class Strategy(Strategy):
         "debug_log": {"type": "list", "default": []},
         "base_available": {"type": "float", "default": 0.0},
         "counter_available": {"type": "float", "default": 0.0},
-        "trend_guard_active": {"type": "bool", "default": False},
         "regimes_updated_at": {"type": "string", "default": ""},
         "account_snapshot_updated_at": {"type": "string", "default": ""},
         "last_balance_log_signature": {"type": "string", "default": ""},
@@ -64,7 +77,6 @@ class Strategy(Strategy):
             "debug_log": ["init cancel all orders"],
             "base_available": 0.0,
             "counter_available": 0.0,
-            "trend_guard_active": False,
             "regimes_updated_at": "",
             "account_snapshot_updated_at": "",
             "last_balance_log_signature": "",
@@ -156,6 +168,33 @@ class Strategy(Strategy):
     def _mode(self) -> str:
         return getattr(self.context, "mode", "active") or "active"
 
+    def apply_policy(self):
+        policy = super().apply_policy()
+        risk = str(policy.get("risk_posture") or "normal").lower()
+        priority = str(policy.get("priority") or "normal").lower()
+
+        step_map = {"cautious": 0.008, "normal": 0.012, "assertive": 0.018}
+        recenter_map = {"cautious": 0.035, "normal": 0.05, "assertive": 0.07}
+        levels_map = {"cautious": 4, "normal": 6, "assertive": 8}
+        delay_map = {"cautious": 500, "normal": 250, "assertive": 120}
+
+        if priority in {"low", "background"}:
+            risk = "cautious"
+        elif priority in {"high", "urgent"}:
+            risk = "assertive"
+
+        self.config["grid_step_pct"] = step_map.get(risk, 0.012)
+        self.config["recenter_pct"] = recenter_map.get(risk, 0.05)
+        self.config["grid_levels"] = levels_map.get(risk, 6)
+        self.config["order_call_delay_ms"] = delay_map.get(risk, 250)
+        self.state["policy_derived"] = {
+            "grid_step_pct": self.config["grid_step_pct"],
+            "recenter_pct": self.config["recenter_pct"],
+            "grid_levels": self.config["grid_levels"],
+            "order_call_delay_ms": self.config["order_call_delay_ms"],
+        }
+        return policy
+
     def _price(self) -> float:
         payload = self.context.get_price(self._base_symbol())
         return float(payload.get("price") or 0.0)
@@ -174,25 +213,6 @@ class Strategy(Strategy):
         self.state["regimes"] = self._regime_snapshot()
         self.state["regimes_updated_at"] = datetime.now(timezone.utc).isoformat()
 
-    def _trend_guard_status(self) -> tuple[bool, str]:
-        if not bool(self.config.get("enable_trend_guard", True)):
-            return False, "disabled"
-
-        reversal_max = float(self.config.get("trend_guard_reversal_max", 0.25) or 0.0)
-        regimes = self.state.get("regimes") or self._regime_snapshot()
-        d1 = (regimes.get("1d") or {}) if isinstance(regimes, dict) else {}
-        h4 = (regimes.get("4h") or {}) if isinstance(regimes, dict) else {}
-        d1_trend = str((d1.get("trend") or {}).get("state") or "unknown")
-        h4_trend = str((h4.get("trend") or {}).get("state") or "unknown")
-        d1_rev = float((d1.get("reversal") or {}).get("score") or 0.0)
-        h4_rev = float((h4.get("reversal") or {}).get("score") or 0.0)
-
-        strong_trend = d1_trend in {"bull", "bear"} and d1_trend == h4_trend
-        weak_reversal = max(d1_rev, h4_rev) <= reversal_max
-        active = bool(strong_trend and weak_reversal)
-        reason = f"1d={d1_trend} 4h={h4_trend} rev={max(d1_rev, h4_rev):.3f}"
-        return active, reason
-
     def _recenter_threshold_pct(self) -> float:
         base_threshold = float(self.config.get("recenter_pct", 0.05) or 0.05)
         atr_multiplier = float(self.config.get("recenter_atr_multiplier", 0.35) or 0.0)
@@ -730,18 +750,6 @@ class Strategy(Strategy):
         desired_sides = self._desired_sides()
 
         mode = self._mode()
-        guard_active, guard_reason = self._trend_guard_status()
-        self.state["trend_guard_active"] = guard_active
-
-        if mode == "active" and guard_active:
-            self._log(f"trend guard active: {guard_reason}")
-            try:
-                self.context.cancel_all_orders()
-            except Exception as exc:
-                self.state["last_error"] = str(exc)
-                self._log(f"trend guard cancel failed: {exc}")
-            self.state["last_action"] = "trend_guard"
-            return {"action": "guard", "price": price, "reason": guard_reason}
 
         if mode != "active":
             if open_order_count > 0:
@@ -896,6 +904,37 @@ class Strategy(Strategy):
         self._log(f"hold at {price} dev {deviation:.4f}")
         return {"action": "hold" if mode == "active" else "plan", "price": price, "deviation": deviation}
 
+    def report(self):
+        snapshot = self.context.get_strategy_snapshot() if hasattr(self.context, "get_strategy_snapshot") else {}
+        return {
+            "identity": snapshot.get("identity", {}),
+            "control": snapshot.get("control", {}),
+            "fit": dict(getattr(self, "STRATEGY_PROFILE", {}) or {}),
+            "position": {
+                "balances": {
+                    "base_available": self.state.get("base_available", 0.0),
+                    "counter_available": self.state.get("counter_available", 0.0),
+                },
+                "open_orders": self.state.get("orders") or [],
+                "exposure": "grid",
+            },
+            "state": {
+                "center_price": self.state.get("center_price", 0.0),
+                "last_price": self.state.get("last_price", 0.0),
+                "last_action": self.state.get("last_action", "idle"),
+                "open_order_count": self.state.get("open_order_count", 0),
+                "regimes_updated_at": self.state.get("regimes_updated_at", ""),
+            },
+            "assessment": {
+                "confidence": None,
+                "uncertainty": None,
+                "reason": "structure-based grid management",
+                "warnings": [w for w in [self._config_warning()] if w],
+                "policy": dict(self.config.get("policy") or {}),
+            },
+            "execution": snapshot.get("execution", {}),
+        }
+
     def render(self):
         # Refresh the market-derived display values on render so the dashboard
         # reflects the same inputs the strategy would use on the next tick.
@@ -924,10 +963,6 @@ class Strategy(Strategy):
                 {"type": "metric", "label": "15m", "value": ((self.state.get('regimes') or {}).get('15m') or {}).get('trend', {}).get('state', 'n/a')},
                 {"type": "metric", "label": f"{self._base_symbol()} avail", "value": round(float(self.state.get("base_available") or 0.0), 8)},
                 {"type": "metric", "label": f"{self.context.counter_currency or 'USD'} avail", "value": round(float(self.state.get("counter_available") or 0.0), 8)},
-                *([
-                    {"type": "metric", "label": "trend guard active", "value": "on"},
-                    {"type": "text", "label": "trend guard reason", "value": "higher-timeframe trend conflict"},
-                ] if self.state.get("trend_guard_active") else []),
                 *([
                     {"type": "text", "label": "config warning", "value": warning},
                 ] if (warning := self._config_warning()) else []),

+ 18 - 0
strategies/hello_world.md

@@ -0,0 +1,18 @@
+# Hello World
+
+Minimal demo strategy for persistence and policy plumbing.
+
+## Best used when
+- you want a smoke test
+- you want to verify policy storage and reporting
+- you want a tiny example strategy with no trading logic
+
+## Avoid when
+- you need real market behavior
+- you need execution logic
+- you need Hermes fit beyond a demo surface
+
+## Notes
+- This strategy increments a counter on every tick.
+- It is useful for tests, not for trading.
+- It still exposes `report()` and policy plumbing.

+ 32 - 0
strategies/hello_world.py

@@ -5,6 +5,12 @@ from src.trader_mcp.strategy_sdk import Strategy
 
 class Strategy(Strategy):
     LABEL = "Hello World"
+    STRATEGY_PROFILE = {
+        "expects": {},
+        "avoids": {},
+        "risk_profile": "demo",
+        "capabilities": ["demo"],
+    }
     TICK_MINUTES = 0.2
     CONFIG_SCHEMA = {
         "label": {"type": "string", "default": "hello world"},
@@ -21,6 +27,32 @@ class Strategy(Strategy):
         self.state["counter"] += 1
         return self.state["counter"]
 
+    def apply_policy(self):
+        policy = super().apply_policy()
+        self.state["policy_derived"] = dict(policy)
+        return policy
+
+    def report(self):
+        snapshot = self.context.get_strategy_snapshot() if hasattr(self.context, "get_strategy_snapshot") else {}
+        return {
+            "identity": snapshot.get("identity", {}),
+            "control": snapshot.get("control", {}),
+            "fit": dict(getattr(self, "STRATEGY_PROFILE", {}) or {}),
+            "position": snapshot.get("position", {}),
+            "state": {
+                "counter": self.state["counter"],
+                "label": self.config.get("label", "hello world"),
+            },
+            "assessment": {
+                "confidence": None,
+                "uncertainty": None,
+                "reason": "demo strategy",
+                "warnings": [],
+                "policy": dict(self.config.get("policy") or {}),
+            },
+            "execution": snapshot.get("execution", {}),
+        }
+
     def render(self):
         return {
             "widgets": [

+ 32 - 0
strategies/trend_follower.md

@@ -0,0 +1,32 @@
+# Trend Follower
+
+Directional strategy for confirmed momentum.
+
+## Best used when
+- trend is strong and persistent
+- structure supports continuation
+- liquidity is normal
+- Hermes wants to follow momentum instead of fading it
+
+## Avoid when
+- price is range-bound
+- trend strength is weak or noisy
+- event risk is high
+- the market is too chaotic for clean continuation
+
+## Core parameters
+- `trend_timeframe`: regime timeframe to inspect
+- `trend_strength_min`: minimum strength to act
+- `entry_offset_pct`: offset from market for entries
+- `exit_offset_pct`: offset for exits or reversals
+- `order_size`: base size for an entry
+- `cooldown_ticks`: pause between actions
+
+## Hermes policy mapping
+- `risk_posture` adjusts strength threshold and offsets
+- `priority` adjusts urgency and cooldown
+
+## Notes
+- Hermes decides when trend following is allowed.
+- Trader maps policy to concrete order behavior.
+- The strategy reports signal, strength, and policy-derived settings.

+ 224 - 0
strategies/trend_follower.py

@@ -0,0 +1,224 @@
+from __future__ import annotations
+
+from datetime import datetime, timezone
+
+from src.trader_mcp.strategy_sdk import Strategy
+from src.trader_mcp.logging_utils import log_event
+
+
+class Strategy(Strategy):
+    LABEL = "Trend Follower"
+    STRATEGY_PROFILE = {
+        "expects": {
+            "trend": "strong",
+            "volatility": "moderate",
+            "event_risk": "low",
+            "liquidity": "normal",
+        },
+        "avoids": {
+            "trend": "range",
+            "volatility": "chaotic",
+            "event_risk": "high",
+            "liquidity": "thin",
+        },
+        "risk_profile": "growth",
+        "capabilities": ["trend_capture", "momentum_following", "position_persistence"],
+    }
+    TICK_MINUTES = 0.5
+    CONFIG_SCHEMA = {
+        "trend_timeframe": {"type": "string", "default": "1h"},
+        "trend_strength_min": {"type": "float", "default": 0.65, "min": 0.0, "max": 1.0},
+        "entry_offset_pct": {"type": "float", "default": 0.003, "min": 0.0, "max": 1.0},
+        "exit_offset_pct": {"type": "float", "default": 0.002, "min": 0.0, "max": 1.0},
+        "order_size": {"type": "float", "default": 0.0, "min": 0.0},
+        "max_order_size": {"type": "float", "default": 0.0, "min": 0.0},
+        "cooldown_ticks": {"type": "int", "default": 2, "min": 0, "max": 1000},
+        "debug_orders": {"type": "bool", "default": True},
+    }
+    STATE_SCHEMA = {
+        "last_price": {"type": "float", "default": 0.0},
+        "last_action": {"type": "string", "default": "idle"},
+        "last_error": {"type": "string", "default": ""},
+        "debug_log": {"type": "list", "default": []},
+        "last_signal": {"type": "string", "default": "neutral"},
+        "last_strength": {"type": "float", "default": 0.0},
+        "cooldown_remaining": {"type": "int", "default": 0},
+        "last_order_at": {"type": "float", "default": 0.0},
+        "last_order_price": {"type": "float", "default": 0.0},
+    }
+
+    def init(self):
+        return {
+            "last_price": 0.0,
+            "last_action": "idle",
+            "last_error": "",
+            "debug_log": ["init trend follower"],
+            "last_signal": "neutral",
+            "last_strength": 0.0,
+            "cooldown_remaining": 0,
+            "last_order_at": 0.0,
+            "last_order_price": 0.0,
+        }
+
+    def _log(self, message: str) -> None:
+        state = getattr(self, "state", {}) or {}
+        log = list(state.get("debug_log") or [])
+        log.append(message)
+        state["debug_log"] = log[-12:]
+        self.state = state
+        log_event("trend", message)
+
+    def _base_symbol(self) -> str:
+        return (self.context.base_currency or self.context.market_symbol or "XRP").split("/")[0].upper()
+
+    def _market_symbol(self) -> str:
+        return self.context.market_symbol or f"{self._base_symbol().lower()}usd"
+
+    def _price(self) -> float:
+        payload = self.context.get_price(self._base_symbol())
+        return float(payload.get("price") or 0.0)
+
+    def _trend_snapshot(self) -> dict:
+        tf = str(self.config.get("trend_timeframe", "1h") or "1h")
+        try:
+            return self.context.get_regime(self._base_symbol(), tf)
+        except Exception as exc:
+            self._log(f"trend lookup failed: {exc}")
+            return {"error": str(exc)}
+
+    def apply_policy(self):
+        policy = super().apply_policy()
+        risk = str(policy.get("risk_posture") or "normal").lower()
+        priority = str(policy.get("priority") or "normal").lower()
+
+        strength_map = {"cautious": 0.8, "normal": 0.65, "assertive": 0.5}
+        entry_map = {"cautious": 0.002, "normal": 0.003, "assertive": 0.005}
+        exit_map = {"cautious": 0.0015, "normal": 0.002, "assertive": 0.003}
+        cooldown_map = {"cautious": 4, "normal": 2, "assertive": 1}
+        size_map = {"cautious": 0.5, "normal": 1.0, "assertive": 1.5}
+
+        if priority in {"low", "background"}:
+            risk = "cautious"
+        elif priority in {"high", "urgent"}:
+            risk = "assertive"
+
+        self.config["trend_strength_min"] = strength_map.get(risk, 0.65)
+        self.config["entry_offset_pct"] = entry_map.get(risk, 0.003)
+        self.config["exit_offset_pct"] = exit_map.get(risk, 0.002)
+        self.config["cooldown_ticks"] = cooldown_map.get(risk, 2)
+        self.config["order_size"] = size_map.get(risk, 1.0)
+        self.state["policy_derived"] = {
+            "trend_strength_min": self.config["trend_strength_min"],
+            "entry_offset_pct": self.config["entry_offset_pct"],
+            "exit_offset_pct": self.config["exit_offset_pct"],
+            "cooldown_ticks": self.config["cooldown_ticks"],
+            "order_size": self.config["order_size"],
+        }
+        return policy
+
+    def _trend_strength(self) -> tuple[str, float]:
+        regime = self._trend_snapshot()
+        trend = regime.get("trend") or {}
+        direction = str(trend.get("state") or trend.get("direction") or "unknown")
+        try:
+            strength = float(trend.get("strength") or 0.0)
+        except Exception:
+            strength = 0.0
+        return direction, strength
+
+    def _suggest_amount(self, price: float) -> float:
+        amount = float(self.config.get("order_size", 0.0) or 0.0)
+        max_order = float(self.config.get("max_order_size", 0.0) or 0.0)
+        if max_order > 0:
+            amount = min(amount, max_order)
+        return max(amount, 0.0)
+
+    def on_tick(self, tick):
+        self.state["last_error"] = ""
+        self._log(f"tick alive price={self.state.get('last_price') or 0.0}")
+        price = self._price()
+        self.state["last_price"] = price
+
+        if int(self.state.get("cooldown_remaining") or 0) > 0:
+            self.state["cooldown_remaining"] = int(self.state.get("cooldown_remaining") or 0) - 1
+            self.state["last_action"] = "cooldown"
+            return {"action": "cooldown", "price": price}
+
+        direction, strength = self._trend_strength()
+        self.state["last_signal"] = direction
+        self.state["last_strength"] = strength
+
+        if strength < float(self.config.get("trend_strength_min", 0.65) or 0.65):
+            self.state["last_action"] = "hold"
+            return {"action": "hold", "price": price, "reason": "trend too weak", "strength": strength}
+
+        amount = self._suggest_amount(price)
+        if amount <= 0:
+            self.state["last_action"] = "hold"
+            return {"action": "hold", "price": price, "reason": "no usable size"}
+
+        side = "buy" if direction in {"bull", "up", "long"} else "sell"
+        offset = float(self.config.get("entry_offset_pct", 0.003) or 0.0)
+        if side == "buy":
+            order_price = round(price * (1 + offset), 8)
+        else:
+            order_price = round(price * (1 - offset), 8)
+
+        try:
+            if self.config.get("debug_orders", True):
+                self._log(f"{side} trend amount={amount:.6g} price={order_price} strength={strength:.3f}")
+            result = self.context.place_order(
+                side=side,
+                order_type="limit",
+                amount=amount,
+                price=order_price,
+                market=self._market_symbol(),
+            )
+            self.state["cooldown_remaining"] = int(self.config.get("cooldown_ticks", 2) or 2)
+            self.state["last_order_at"] = datetime.now(timezone.utc).timestamp()
+            self.state["last_order_price"] = order_price
+            self.state["last_action"] = f"{side}_trend"
+            return {"action": side, "price": order_price, "amount": amount, "result": result, "strength": strength}
+        except Exception as exc:
+            self.state["last_error"] = str(exc)
+            self._log(f"trend order failed: {exc}")
+            self.state["last_action"] = "error"
+            return {"action": "error", "price": price, "error": str(exc)}
+
+    def report(self):
+        snapshot = self.context.get_strategy_snapshot() if hasattr(self.context, "get_strategy_snapshot") else {}
+        return {
+            "identity": snapshot.get("identity", {}),
+            "control": snapshot.get("control", {}),
+            "fit": dict(getattr(self, "STRATEGY_PROFILE", {}) or {}),
+            "position": snapshot.get("position", {}),
+            "state": {
+                "last_price": self.state.get("last_price", 0.0),
+                "last_action": self.state.get("last_action", "idle"),
+                "last_signal": self.state.get("last_signal", "neutral"),
+                "last_strength": self.state.get("last_strength", 0.0),
+                "cooldown_remaining": self.state.get("cooldown_remaining", 0),
+            },
+            "assessment": {
+                "confidence": None,
+                "uncertainty": None,
+                "reason": "trend capture",
+                "warnings": [],
+                "policy": dict(self.config.get("policy") or {}),
+            },
+            "execution": snapshot.get("execution", {}),
+        }
+
+    def render(self):
+        return {
+            "widgets": [
+                {"type": "metric", "label": "market", "value": self._market_symbol()},
+                {"type": "metric", "label": "price", "value": round(float(self.state.get("last_price") or 0.0), 6)},
+                {"type": "metric", "label": "signal", "value": self.state.get("last_signal", "neutral")},
+                {"type": "metric", "label": "strength", "value": round(float(self.state.get("last_strength") or 0.0), 4)},
+                {"type": "metric", "label": "state", "value": self.state.get("last_action", "idle")},
+                {"type": "metric", "label": "cooldown", "value": int(self.state.get("cooldown_remaining") or 0)},
+                {"type": "text", "label": "error", "value": self.state.get("last_error", "") or "none"},
+                {"type": "log", "label": "debug log", "lines": self.state.get("debug_log") or []},
+            ]
+        }

+ 64 - 8
tests/test_strategies.py

@@ -8,7 +8,9 @@ from fastapi.testclient import TestClient
 from src.trader_mcp import strategy_registry, strategy_store
 from src.trader_mcp.server import app
 from src.trader_mcp.strategy_context import StrategyContext
+from src.trader_mcp.strategy_sdk import Strategy as BaseStrategy
 from strategies.grid_trader import Strategy as GridStrategy
+from strategies.trend_follower import Strategy as TrendStrategy
 
 
 STRATEGY_CODE = '''
@@ -97,16 +99,16 @@ def test_stop_loss_strategy_loads_with_aligned_regime_config(tmp_path):
         strategy_registry.STRATEGIES_DIR = tmp_path / "strategies"
         strategy_registry.STRATEGIES_DIR.mkdir()
         (strategy_registry.STRATEGIES_DIR / "grid_trader.py").write_text((Path(__file__).resolve().parents[1] / "strategies" / "grid_trader.py").read_text())
-        (strategy_registry.STRATEGIES_DIR / "stop_loss_trader.py").write_text((Path(__file__).resolve().parents[1] / "strategies" / "stop_loss_trader.py").read_text())
+        (strategy_registry.STRATEGIES_DIR / "exposure_protector.py").write_text((Path(__file__).resolve().parents[1] / "strategies" / "exposure_protector.py").read_text())
 
         grid_defaults = strategy_registry.get_strategy_default_config("grid_trader")
-        stop_defaults = strategy_registry.get_strategy_default_config("stop_loss_trader")
+        stop_defaults = strategy_registry.get_strategy_default_config("exposure_protector")
 
         assert grid_defaults["trade_sides"] == "both"
-        assert grid_defaults["trend_guard_reversal_max"] == 0.25
-        assert stop_defaults["regime_timeframes"] == ["1d", "4h", "1h", "15m"]
-        assert stop_defaults["trend_enter_threshold"] == 0.7
-        assert stop_defaults["trend_exit_threshold"] == 0.45
+        assert grid_defaults["grid_step_pct"] == 0.012
+        assert stop_defaults["trail_distance_pct"] == 0.03
+        assert stop_defaults["rebalance_target_ratio"] == 0.5
+        assert stop_defaults["min_rebalance_seconds"] == 300
     finally:
         strategy_store.DB_PATH = original_db
         strategy_registry.STRATEGIES_DIR = original_dir
@@ -203,7 +205,6 @@ def test_grid_missing_order_triggers_full_rebuild(monkeypatch):
     monkeypatch.setattr(strategy, "_sync_open_orders_state", fake_sync_open_orders_state)
     monkeypatch.setattr(strategy, "_price", lambda: 1.3285)
     monkeypatch.setattr(strategy, "_refresh_regimes", lambda: None)
-    monkeypatch.setattr(strategy, "_trend_guard_status", lambda: (False, "disabled"))
     monkeypatch.setattr(strategy, "_grid_refresh_paused", lambda: False)
     monkeypatch.setattr(strategy, "_recenter_threshold_pct", lambda: 0.5)
 
@@ -262,7 +263,6 @@ def test_grid_side_imbalance_triggers_full_rebuild(monkeypatch):
     monkeypatch.setattr(strategy, "_refresh_balance_snapshot", lambda: None)
     monkeypatch.setattr(strategy, "_price", lambda: 1.3915)
     monkeypatch.setattr(strategy, "_refresh_regimes", lambda: None)
-    monkeypatch.setattr(strategy, "_trend_guard_status", lambda: (False, "disabled"))
     monkeypatch.setattr(strategy, "_grid_refresh_paused", lambda: False)
     monkeypatch.setattr(strategy, "_recenter_threshold_pct", lambda: 0.5)
 
@@ -329,3 +329,59 @@ def test_grid_stop_cancels_all_open_orders():
     assert strategy.context.cancelled is True
     assert strategy.state["open_order_count"] == 0
     assert strategy.state["last_action"] == "stopped"
+
+
+def test_base_strategy_report_uses_context_snapshot():
+    class FakeContext:
+        id = "s-1"
+        account_id = "acct-1"
+        market_symbol = "xrpusd"
+        base_currency = "XRP"
+        counter_currency = "USD"
+        mode = "active"
+
+        def get_strategy_snapshot(self):
+            return {
+                "identity": {"strategy_id": "s-1", "strategy_name": "Demo", "account_id": "acct-1", "market": "xrpusd", "base_currency": "XRP", "quote_currency": "USD"},
+                "control": {"enabled_state": "on", "mode": "active"},
+                "position": {"balances": [{"asset_code": "XRP", "available": 1.0}]},
+                "orders": {"open_orders": [{"id": "o1"}]},
+                "execution": {"execution_quality": "good"},
+            }
+
+    class DemoStrategy(BaseStrategy):
+        LABEL = "Demo"
+
+    report = DemoStrategy(FakeContext(), {}).report()
+    assert report["identity"]["strategy_id"] == "s-1"
+    assert report["control"]["mode"] == "active"
+    assert report["position"]["open_orders"][0]["id"] == "o1"
+
+
+def test_trend_follower_uses_policy_and_reports_fit():
+    class FakeContext:
+        id = "s-2"
+        account_id = "acct-2"
+        client_id = "cid-2"
+        mode = "active"
+        market_symbol = "xrpusd"
+        base_currency = "XRP"
+        counter_currency = "USD"
+
+        def get_price(self, symbol):
+            return {"price": 1.2}
+
+        def get_regime(self, symbol, timeframe="1h"):
+            return {"trend": {"state": "bull", "strength": 0.9}}
+
+        def place_order(self, **kwargs):
+            return {"ok": True, "order": kwargs}
+
+        def get_strategy_snapshot(self):
+            return {"identity": {}, "control": {}, "position": {}, "orders": {}, "execution": {}}
+
+    strat = TrendStrategy(FakeContext(), {"order_size": 1.5})
+    strat.apply_policy()
+    report = strat.report()
+    assert report["fit"]["risk_profile"] == "growth"
+    assert strat.state["policy_derived"]["order_size"] > 0