<< All versions

Skill v1.0.0

currentAutomated scan100/100
9600dev/mmr/mmr-loop-skill
──Details
PublishedOctober 29, 2026 at 06:29 AM
Content Hashsha256:3bba819b42f27e66...
Git SHA5ac9b3d94454
──Files
Files (1 file, 10.3 KB)
SKILL.md10.3 KBactive
SKILL.md · 162 lines · 10.3 KB

name: mmr-loop-skill description: Autonomous trading loop for the MMR platform. Continuously monitors portfolio, scans for opportunities, creates trade proposals, and manages risk. Requires the 'mmr' skill to be loaded first. Use when the user asks to start monitoring, trading autonomously, or running a trading loop. metadata: author: mmr version: "1.0" dependencies: mmr-skill


MMR Trading Loop

This skill turns the MMR trading platform into an autonomous trading agent. It implements a phased state machine that continuously monitors your portfolio, scans for opportunities, and creates trade proposals for your review.

Important: This skill requires the mmr skill to be loaded first. Load both:

await load_skill("mmr", "all")
await load_skill("mmr-loop", "all")

Where the loop's commands run

The loop's core cycle is DuckDB-backed (portfolio_risk reads the group store, session_status counts proposals, propose writes one). While the mmr Docker container is up, the live DuckDB lives in the mmr_db_data volume and those calls must execute inside the container — from the host they return ShadowedDatabaseError and the cycle stalls at PROPOSE. MMRHelpers routes through docker exec automatically when the .db_in_container_volume marker is present and mmr-mmr-1 is running; confirm once at loop start:

python
mode = await MMRHelpers.exec_mode()
assert mode["mode"] == "container" or not mode["db_marker_present"], mode

The "run sequentially, DuckDB is single-writer" guidance below is about the lock, not about which database file is open — both apply. See mmr-skill § Where commands run for overrides (MMR_SKILL_EXEC, MMR_CONTAINER).

How It Works

The loop runs a repeating cycle with four phases:

PRE-FLIGHT → MONITOR → ANALYZE → PROPOSE → DIGEST → (sleep) → PRE-FLIGHT → ...

PRE-FLIGHT: Checks status() for trader_service connectivity and IB Gateway upstream connection. If IB Gateway can't reach IBKR servers, the cycle skips directly to DIGEST — no wasted API calls that would timeout.

MONITOR: Quick health check. Calls portfolio_snapshot() and portfolio_diff() (~500 tokens). If nothing moved (all positions unchanged), skips directly to DIGEST — no wasted API calls or context.

ANALYZE: Only runs when MONITOR finds something interesting (positions moved, market open, new cycle). Runs portfolio_risk() for warnings, scans with ideas() using rotating presets (momentum → mean-reversion → breakout → volatile → gap-down), checks news for held positions.

PROPOSE: Creates trade proposals via propose() with auto-sizing (ATR-adjusted), group tagging, confidence scores, and reasoning. Never auto-executes — proposals wait for user approval.

DIGEST: Writes cycle summary to memory (persists across compactions), compacts context with compact("drop-helpers-results"), sleeps until next cycle.

Starting the Loop

To start the trading loop, register hooks and initialize state:

python
result = await load_skill("mmr", "all")
result = await load_skill("mmr-loop", "all")
# Initialize the loop engine
await TradingLoop.start()

The loop will run until you say "stop" or call await TradingLoop.stop().

User Interaction

While the loop is running, you can interrupt at any time:

  • "stop" or "pause" — stops the loop
  • "approve 42" — approve a pending proposal
  • "reject 42" — reject a proposal
  • "status" — get current loop state and cycle count
  • "skip" — skip to next cycle immediately
  • Any other question — the loop pauses, answers, then resumes

Configuration

The loop reads configuration from the TradingLoop.config dict. Override before starting:

python
TradingLoop.config["scan_interval_seconds"] = 300 # 5 minutes between cycles
TradingLoop.config["scan_presets"] = ["momentum", "mean-reversion"]
TradingLoop.config["max_proposals_per_cycle"] = 1
TradingLoop.config["auto_approve"] = False # NEVER set to True without understanding the risks

See references/LOOP_CONFIG.md for full configuration reference.

Safety Boundaries

  1. Proposals, not trades: The loop creates proposals but NEVER auto-executes. You approve or reject.
  2. Position limits: Respects max_positions from position_sizing.yaml. Stops proposing at the limit.
  3. Group budgets: Checks group allocation budgets before proposing. Over-budget = warning, not block.
  4. Gates apply to OPENS only; exits are never refused: The risk gate, trading filter, leverage check, and any approval gate run on exposure-increasing orders. Any order that reduces the live broker position — a close, a protective stop, a position-reducing SELL — is classified exit-class server-side and is never blocked. (The old client skip_risk_gate flag is deprecated and ignored — the exit-class classifier decides, not a flag.)
  5. Gates fail closed on opens (expect brief warm-up refusals after a restart): When a critical input can't be read (daily PnL, account value, a price to value a market order), opening exposure is refused with a reason naming the missing datum. For a few seconds after a trader_service restart, while IB feeds warm up, market opens can be refused with "…could not be read — fail-closed" — this is by design, self-clears quickly, and never affects closes. Don't treat these as a fault; skip that cycle's open and retry next cycle. The hourly rate limit now counts real ORDER_SUBMITTED events.
  6. Proposal state machine: Terminal statuses (EXECUTED / REJECTED / FAILED / EXPIRED) are immutable. If approve() fails at the broker (margin rejection, bracket rollback), OR the sized amount doesn't cover one whole share (approve floors amount→shares and refuses at zero — it no longer bumps to 1 share), the proposal moves to FAILED — create a new proposal rather than re-approving the old one. On a tiny amount / very high-priced instrument, raise the amount.
  7. Scanner fails loudly: ideas() with location= raises on "no results" rather than returning an empty list, so the loop will see a real error for misconfigured markets instead of silently making zero proposals. Log the error, skip the scan for that cycle, and continue.
  8. Signed exposure in risk reports: portfolio_risk() returns net_exposure_pct, long_exposure_pct, short_exposure_pct alongside gross_exposure_pct. For long-only loops these are effectively the same; for long/short loops, read net_exposure_pct first — correlation-cluster warnings now fire on net, so a correlated long/short hedge pair won't false-alarm.
  9. Strategy gauntlet gate ("no hash, no live"): If the loop ever deploys or enables a strategy, an auto_execute strategy is refused without a PASS record from mmr strategies gauntlet <module> --class <Class> for its exact source hash (no override flag). Not part of the propose/approve path, but a hard boundary if you touch the roster.
  10. Opt-in approval hardening (default OFF — not on the running stack): Three Phase-2 defenses exist but ship disabled (see docs/SAFETY_ROADMAP.md). When enabled: a server-side notional tier requires an out-of-band approver_key for an above-threshold OPEN (exits exempt); proposal_show splits trusted (server-recomputed facts) from untrusted (proposer text) — decide only on trusted; and an MMR_ROLE=proposer context cannot approve/buy/sell/resize. Do not assume any of these are active unless configured.
  11. Context management: Aggressive compaction keeps context at ~12K tokens. Can run indefinitely.

What Gets Written to Memory

Each cycle writes a summary entry:

Key: trading_loop_cycle_{N}
Summary: "Cycle N — Portfolio $67K (+$20 daily). Scanned momentum. Created 1 proposal (BHP BUY $3,600). HHI 0.064, no warnings."

You can review cycle history anytime:

python
keys = read_memory_keys()
for k in keys:
if k["key"].startswith("trading_loop_cycle_"):
print(f'{k["key"]}: {k["summary"]}')

Position Tracking

The loop has a built-in position monitor that checks tracked positions between cycles. When a position hits its stop-loss or take-profit threshold, the LLM is alerted. Between alerts, monitoring happens silently in the hook — no LLM turns are wasted.

python
# Track positions after entering trades
await TradingLoop.track_position("BHP", "LONG", entry=50.15, qty=350, stop_pct=-1.5, add_pct=2.0)
await TradingLoop.track_position("PLS", "SHORT", entry=4.58, qty=3800)
# Or use module-level convenience functions
await track_position("BHP", "LONG", 50.15, 350)
# Check what's being tracked
status = await TradingLoop.status()
# status["tracked_symbols"] → ["BHP", "PLS"]
# Stop tracking
await TradingLoop.untrack_position("BHP")
await TradingLoop.untrack_all() # clear all

The monitor uses snapshots_batch() for efficiency (~4s for all symbols vs ~4s per symbol). Configure timing with monitor_interval_seconds (default 180s). Default thresholds: monitor_stop_pct=-1.5, monitor_add_pct=2.0.

Quick Reference

FunctionDescription
await TradingLoop.start()Start the loop (registers hooks)
await TradingLoop.stop()Stop the loop (unregisters hooks)
await TradingLoop.status()Get running state, cycle count, tracked positions
await TradingLoop.track_position(sym, side, entry, qty, ...)Add position to monitor watchlist
await TradingLoop.untrack_position(sym)Remove from watchlist
await TradingLoop.untrack_all()Clear all tracked positions
await start_trading_loop(**overrides)Start with config overrides in one call
await stop_trading_loop()Alias for TradingLoop.stop()
TradingLoop.config["key"] = valueChange config before or during loop

Architecture Notes

  • Hook sleeps internally via asyncio.sleep() — no wasted LLM turns between cycles
  • Timer-based gating: pure elapsed time, no iteration-modulo tricks
  • Position monitor runs between full cycles, only alerts the LLM when thresholds are breached
  • Uses snapshots_batch() for efficient multi-symbol price checks (~4s total)
  • Uses delegate_task() for parallel data gathering (portfolio + risk + scan simultaneously)
  • Context stays at ~12K tokens after compaction across unlimited cycles
  • Rotating scan presets ensure different patterns are checked each cycle
All versions