senpi-strategy-ops

v2026.09.24

Deploy / monitor / close a NAMED Senpi trading strategy. Monitoring is on demand: never an agent-turn cron to watch a strategy (a model call per firing) — the runtime supervises it at zero model cost, and there is no paper-trading mode ($10 floor = the test). Use when the user names a strategy to run — "install spider", "deploy polar", "set up kodiak", "reinstall athena", "run the spider strategy", "is my strategy live?", "what am I running", "list my strategies" (→ status.py), A STRATEGY IS A PACKAGE, NEVER A SKILL: "install/reinstall <name>" is `deploy.py create <id>` here — never `openclaw skills install`, never clawhub, and never a GitHub user or repo named after the strategy. A package missing from disk is normal (it was closed, or never fetched) and `deploy.py` fetches it by id. "are my positions protected? / do they have a stop-loss (DSL)?", "stop/close/uninstall polar" — and for teardown like "close all strategies", "return funds to main", "tear everything down" (→ close.py --all). ALWAYS tear down via close.py, never a raw strategy_close (that strands the runtime). ops deploys / closes / monitors; it does NOT author or edit strategy files — an edit ("make my live strategy more aggressive", change leverage/sizing/DSL) is authored in senpi-strategy-author, the only skill that knows the scanner / yaml / DSL schema. A strategy is a PACKAGE (strategy.yaml + one runtime.yaml per instance + scanners/) the runtime supervises in-process — no scanner daemon. `deploy.py create <id> --budget <usd>` takes a package live end to end (it gates the package, then runs the runtime's detached deploy job; watch with `senpi deploy status`); close.py tears down (stop runtime + strategy_close → flattens positions, returns funds). Before the budget question ops runs THE WALKTHROUGH (Step 0.75): what the template does, how it is set, the two levers worth shifting — all in bullets and plain words, never a config key — and its name: every template deploys as the user's own fork, named after their Senpi username (`deploy.py create <template>` → `ignas-phalanx`, spoken as "Ignas's Phalanx"; `--name` for their own words), as-is or with levers moved. The id (spider, polar, kodiak) is the package folder. NOT for choosing WHICH strategy (senpi-strategy-discover) or authoring / editing the strategy files themselves (senpi-strategy-author).

GitHub
Install command
npx skhub add senpi-ai/senpi-strategy-ops
Markdown
SKILL.md

Senpi Strategy Ops — deploy, monitor, close

A strategy is a package: strategy.yaml (the deploy manifest) + one runtime.yaml per instance + <instance>/scanners/. The runtime spawns and supervises each scanners/scan.py, calling scan(inputs, ctx) every interval_seconds and owning everything downstream — signal validation, sizing/execution, the two-phase DSL exit, risk guard-rails. There is no separate scanner daemon, no push_signal. Ops owns the deployed lifecycle. Deploy is one verb, run as a detached job: funds preflight → wallet create+fund → runtime install → one observed scanner tick. It returns in ~1s and you poll until it is terminal.

openclaw senpi validate <recipe-dir>                                    # 0a. does it RUN? records the proof create needs
python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/deploy.py validate <id>              # 0b. preflight — structurally deploy-ready? (no money, nothing installed; a bare id is fetched to disk)
python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/deploy.py fork <id>                  # 0c. their copy on disk — only when levers move before funding
python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/deploy.py create <id> --budget <usd> # 1. THE FUNDED PATH: forks it under their username, proves it, starts the deploy
openclaw senpi deploy status                                            # 2. poll until terminal; read the verified report
python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/status.py                            # what am I running? (+ health)
python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/close.py <id> | --all                # teardown one strategy | EVERY open strategy

"Install"/"reinstall <name>" = deploy.py create <id> — a strategy is a package, not an openclaw/clawhub skill; not on disk just means fetch it by id. Fund through deploy.py create|runtime <id>, not through the bare verb. Both resolve the package, run the structural preflight, then start the runtime's senpi deploy job, poll it, and relay its report verbatim. The wrapper's value is resolution, that structural pass and the verbatim relay — not a gate the verb lacks: the live-universe gate is the verb's own and it fires pre-money. Use the bare verb for the read-only openclaw senpi deploy status, and whenever resolution is not needed. Always tear down through close.py (one <id> or --all) — it deletes the runtime and closes the strategy. A raw strategy_close MCP call closes the strategy but leaves the runtime registered, which collides on the next deploy. Pass the strategy id for a CATALOG strategy (what senpi-strategy-discover hands over, e.g. spider), fetched from the remote if not on disk; for a locally-authored package, pass its DIRECTORY path. Either way the package belongs in the durable strategies root (SENPI_STRATEGIES_DIR if set, else the agent workspace strategies/ dir — normally /data/workspace/strategies) and never inside a skill directory: a package written there is destroyed on the next skill update. An on-disk package is authoritative; an invalid one surfaces its real errors and is never silently replaced by a remote fetch. Mechanics + state machine: references/lifecycle.md · manifest schema: references/strategy-yaml-schema.md.

Deploy — one lifecycle: start the verb, poll status until terminal

status is the gate, not a suggestion. A strategy is LIVE only when the deploy report's overall is live — every instance created + installed + a verified scanner tick observed. installed-unobserved means the tick was NOT seen inside the wait window — not failure and not success; say exactly that. Never report "live" off a started job, a running phase, or an install alone — nor a VERSION off the catalog; quote deploy's own package <id> vX.Y.Z.

Step 0 — resolve which strategy. The user's word ("spider") is a strategy id. Confirm it exists against the registry (curl -s https://raw.githubusercontent.com/Senpi-ai/senpi-skills/refs/heads/main/strategies/catalog.json); no match → hand to senpi-strategy-discover.

Step 0.5 — preflight (required). deploy.py validate <id> reports every structural + render issue in one pass, with no money moved and nothing installed — a bare catalog id is fetched to disk, which is also how it resolves that id to a package directory. It also reports the live universe, the gate the deploy verb enforces pre-money, so validate is the cheap early read of it, not the thing holding it. That read is a network call (market_list_instruments) needing SENPI_AUTH_TOKEN, and it can take a few seconds.

Preflight is two questions, two commands. openclaw senpi validate <recipe-dir> answers does it run — one real tick against live read-only data, no wallet and no funding — and a PASS is also what RECORDS the proof create refuses without. So it is validate → deploy, per instance: one run per instance, pointed at the dir holding that instance's runtime.yaml (the package root for a flat package, the instance's own dir once strategy.yaml lists them), and an edited package needs a fresh openclaw senpi validate before the next create. UNPROVEN (exit 2) is not a pass. deploy.py validate <id> answers the other question, is the package well formed. Do not deploy a package that has not returned PASS. The proof: references/lifecycle.md.

Step 0.75 — the walkthrough (REQUIRED before the budget question). The package is on disk (Step 0.5). Read their username first (user_get_me → userName; none → ask what to call it). Before you ask for a dollar, read strategy.yaml (catalog:) and each instance's runtime.yaml and say it all in bullets — plain words, no YAML, no config keys: what it does (three bullets: the signal · when it enters · how it exits), how it is set (the post-live block's four lines), two levers worth a look (one for tier: starter) — each named by what it controls, never by its key ("How picky it is about crowding — now 65% of the cohort on one side; lower = more trades and more fees, higher = fewer, stronger ones"; "How much of the wallet each position uses — 15%: about $95 of margin per position on your $635"), the fee load and the design budget, and the name as a fact, never a question: "It deploys as Ignas's Phalanx (ignas-phalanx in your strategy list) — say a different name if you want one." Then one question: "Run it as-is, or shift one of these first? Say go and it deploys as-is." A bare "go" / "deploy" / "yes" = as-is under their name; a status check ("did you finish?", "how's it going?") is not a yes. Never a gate, never re-asked; the budget question comes after the answer. Cost class is a fact beside the choice, never a discouragement; named installs get the walkthrough too. Below the design budget, a guardrail removed or a threshold lowered: the consequence in one line and an explicit yes — never "done". The fork is the verb's job: deploy.py create <template> --budget <usd> reads their username itself, copies the template to <username>-<template> under the durable root, rewrites its identity (id, catalog.name, runtime name/group/description, forked_from), proves it and deploys it; --name "<their words>" for a name of their own; deploy.py fork <template> makes the copy first when levers move (the edit path on the fork, then create <dir>). A user ID is never a name; with no username to read, a bare template id is refused. The lever language, the name rules and the fork: references/walkthrough.md.

Step 1 — start the deploy. Budget splits across instances by funding_share, min $10 each (the platform wallet floor) — ask for the amount now — after the walkthrough, never before it — and confirm it. Two tiers, and only the first stops anything: below the $10/wallet floor the deploy refuses; a wallet left with less than its own sizing needs still deploys, with a [W_BUDGET_BELOW_STRATEGY_MIN] warn to relay.

python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/deploy.py create spider --budget 300   # username PurpleFrog → purplefrog-spider, "PurpleFrog's Spider"

It validates locally, starts the job — which itself refuses pre-money on a dead universe — then polls deploy status and prints the verb's report verbatim. Flags: --decision-model <model> (only for a decision_mode: llm action), --tick-wait <s> (tick-observation window, default 120; 0 skips it and can then never report live), --max-wait <s> (ACTIVE-wallet wait, default 150 — an explicit value also becomes deploy.py's own poll budget), --json (clean stdout, notes on stderr).

Re-running create is how you resume, and it never double-funds: the verb reconciles first and adopts a wallet that already exists, then installs whatever is still missing rather than funding a second beside it. The --budget is a hard target: if the live balance can't cover it the deploy refuses with the exact shortfall and NEVER silently funds less (the "$1,000 → $10" failure). Fund/free USDC or confirm a smaller amount, then re-run — never lower --budget to dodge a funding error. When the user names a budget per strategy ("$1k on X, $2k on Y"), deploy each with its own and confirm the split before funding.

deploy.py polls for ~150s and then returns, staying inside the ~180s tool timeout — with one bounded exception, the stale-proof repair (references/refusal-playbook.md), whose note reaches stderr before validation starts, so a call killed mid-repair still says nothing was created: read openclaw senpi deploy status, and do NOT re-run create. A job still running at the lapse is exit 6 / pending with the snapshot and the command to watch it — normal on a slow funding leg or a long scanner interval, not a failure and not a reason to re-run create.

Inside the job, per instance: reconcile → preflight → create → install → observe — the phase names deploy status prints while it runs; an existing live wallet is adopted at reconcile, never duplicated, and only a fresh tick counts at observe. Per-step mechanics: references/lifecycle.md.

Only one deploy runs at a time. A second start is refused, naming the running job — watch that one. There is no cancel verb: undeploying is closing the strategy (close.py <id>), not stopping the job; a wedged deploy abandons itself at a step boundary and frees the slot on its own.

Step 2 — poll openclaw senpi deploy status until it is terminal. While running it prints the phase as an in-progress fact; once terminal, the per-instance report with its evidence quoted (totalFunded, the scanner row's lastRunStatus/runCount).

Terminal outcomes — the exit code and the report's overall are one branch table (for both openclaw senpi deploy status and deploy.py):

exitoverallMeaningWhat to do
0liveevery instance installed and a scanner tick observedreport live + the How it runs block. A warn: line ([W_BUDGET_*]) may ride a live report — relay it; it did not stop the deploy (see the relay contract below)
4installed-unobservedinstalled, no tick seen inside --tick-wait (or --tick-wait 0 skipped the check)say exactly that; check openclaw senpi scanner -r <runtimeId> in a few minutes. External scanners legitimately tick on long intervals. --tick-wait 0 can never report live — nothing was verified
6pendinga wallet still funding, or the job still running when the poll budget ran outre-run the same deploy command — it resumes and adopts the wallet
2refuseda gate said no ([E_FUNDS_*], [E_VALIDATE_*], [E_UNIVERSE_NOT_LIVE], [E_STATE_AMBIGUOUS_WALLETS], [E_INSTANCE_BINDING_UNKNOWN], [E_WALLET_OWNED_BY_OTHER_PACKAGE], [INVALID_REQUEST])do what the refusal's code says — per-code depth in references/refusal-playbook.md; nothing was created past it
3faileda step genuinely failed (backend rejection, install error, scanner erroring)read the quoted cause, fix it, re-run — except [E_INSTALL_INDETERMINATE] (an install whose outcome is UNKNOWN — nothing to fix, and the money may still be out: do its read first) and [E_WALLET_INSTALL_IN_FLIGHT] (a race, not a defect — "fix it" is not "clear the stuck install"; never runtime delete here) and a backend SERR124 (this account's pool of strategy wallets is empty — a per-account condition, nothing was created or debited: tell the user, route it to Senpi support, one later attempt after they confirm, never a retry loop). All three: per references/refusal-playbook.md
5interrupteda gateway restart killed the job mid-runnothing resumes on its own — re-run the deploy; it reconciles

2 is any gate saying no with nothing created past it — the verb's refusals, and deploy.py's own structural preflight when create/runtime fail it (nothing started; fix what it names and re-run — a bare retry refuses identically). 1 is "could not answer", never an outcome, and it is also what an unrecognised status returns, so a new status can never read as success. Branch on the code.

Re-running is always safe. There is no local deploy-state file: the backend strategies and the runtime registry are the record, so re-running the same command reconciles and adopts whatever already exists instead of duplicating it. A wallet still initializing is waited for — but only the statuses that resolve on their own are (a PAUSED strategy never becomes ACTIVE by itself, so it is refused, not waited for — see the PAUSED section of references/refusal-playbook.md).

Behaviour change from the old three-step flow. create used to refuse an already-running <id> and to close a runtime-less one to force a fresh wallet; the verb adopts instead. Two consequences worth saying out loud to the user: deploy never adds funds to a wallet that already exists (ask for $500 against a wallet holding $100 and the report says the $500 was NOT added — top up separately, or close.py and redeploy), and "re-run to get a fresh wallet" is no longer a thing.

Packages with no exit block are refused before anything is funded — worth knowing before you build or pick one; the refusal names the offending instances and computes its own re-check block (references/refusal-playbook.md).

Do NOT improvise. A package strategy is a runtime-supervised scanner — deploy it only via deploy.py create|runtime. Never substitute a raw strategy_create_custom_strategy MCP call: that makes an empty custom-position strategy, not the running scanner, and it carries no attribution. Never reach for openclaw senpi runtime create — it is internal and skips the funds preflight, the attribution and the verified tick. Funding is automatic, drawn from the funding wallet's Hyperliquid perps balance only — spot USDC does not count (move it with transfer_spot_to_perps), and EVM balances do not bridge in. Follow the refusal's code, exactly: references/refusal-playbook.md.

Refusals and warns — the relay contract

Every refusal and warn is rendered by the runtime against the state it actually read, and names its own next step. So:

  • Relay it verbatim — the figure it names, not one beside it (a report carries the requested amount too; quoting that one is the wrong number). Never re-derive a number or a lifecycle claim.
  • Execute the step it names — never improvise one, never widen its scope. If it names a read-only triage, that is the step. Never substitute a destructive escape for a named non-destructive one.
  • If it names no command, that is the answer, not a gap for you to fill. Some reports deliberately carry none, because no safe command exists for that state.
  • W_ is advisory — it blocked nothing. Never report a deploy as failed because of a warn, and never close a wallet over one. A warn rides failed and pending reports too, so overall still decides whether it went through. E_ means refused or failed.
  • One report, one teardown instruction. Where [E_ROLLBACK_INCOMPLETE] appears it owns the cleanup — do that first, follow its command exactly, and tell the user either way: a funded, unwatched wallet is never left unreported.
  • A pre-money gate says what THAT RUN did, not what exists. The universe and proof gates read one instrument list, or the package's own files, and stop before reading this package's live state — so their "nothing was created" is scoped to that run. Never widen it to "there is no wallet": on a resume the package may already own a funded, live wallet, and the unscoped sentence is what funds a second one beside it.

Two money rules the report cannot enforce for you — and, when a code fires, the per-code depth is in references/refusal-playbook.md:

  • Never lower --budget to clear a funding refusal without asking the user. [E_FUNDS_SHORT] names the exact figure it can fund — offer it as a choice, alongside depositing more. [E_FUNDS_BELOW_FLOOR] means no budget is valid: help the user deposit (senpi-deposit-withdraw-transfer), and never suggest a smaller one.
  • Which wallet is which is the USER's call. Where a refusal lists live wallets ([E_STATE_AMBIGUOUS_WALLETS], [E_INSTANCE_BINDING_UNKNOWN], [E_WALLET_OWNED_BY_OTHER_PACKAGE]), relay the list and ask. Triage is read-only — run the read the refusal names (status.py <id>, or openclaw senpi status / strategy_list). Never close or recreate to "start clean": that can tear down a funded live strategy, and a wallet stamped for another package holds someone else's funds.

Report from the structured output, not raw logs

Then always close with the How it runs block below. funded is the backend's totalFunded and the tick line is the scanner row's own fields — quote both verbatim (the document's shape: references/lifecycle.md).

The funded path — deploy.py create|runtime

deploy.py no longer deploys anything itself. Each of its two money-moving subcommands resolves the package (forking a template under the user's Senpi username first — --name for words of their own on create), runs the structural preflight, then starts the same verb — which holds the live-universe gate itself, pre-money — polls it, and prints its report verbatim. Both keep the same flags and the verb's exit codes. The old == 2 habit no longer catches a failure: the pre-verb script exited 2 on failure, this one exits 3, so anything branching on == 2 alone silently treats every failed deploy as a success. Branch on the whole table. create and runtime are one path under two names — either one resumes, adopting whatever already exists.

deploy.py verify <id> is READ-ONLY — it starts no deploy, funds nothing, installs nothing and fetches nothing (on-disk packages only). It quotes MCP strategy_list + openclaw senpi runtime list + openclaw senpi status --json, plus the last deploy job's [W_*] warns when that job was this package's; it never re-derives a status or a number. Exit codes are its own: 0 verified · 3 not verified (it names, per instance, what is missing and the one non-destructive next step for that state) · 1 could not check (a read it needs failed — that is not "not live"; re-read). The resume is always create/runtime, never verify. Where it quotes a wallet's skillName stamp, that is a quote and never proof of who created the wallet: any caller of the creation tool can write any stamp, so a raw-MCP create or an older/differently-cased id of your own renders the same row. Mechanics, the states it will and will not steer at, and the codes it can name: references/lifecycle.md · references/refusal-playbook.md.

Host prerequisites

openclaw + the @senpi-ai/runtime plugin running, new enough to carry the senpi deploy verb — on a skewed box the start fails at exit 1 saying which side is behind, with nothing dispatched: no job, no wallet, no funds (references/lifecycle.md has both directions and the fix). Also SENPI_AUTH_TOKEN exported (the same token the MCP session uses); Python 3 only — no PyYAML/pip needed. Smoke with deploy.py validate <id> first.

Final step — tell the user HOW each strategy runs (REQUIRED on every deploy)

The user just funded a strategy; the last thing they see must explain how the thing they funded actually behaves — not just "it's live." After the live report, ALWAYS close the confirmation with a compact "How it runs" block per deployed strategy (per instance when their configs differ). Read every value from the deployed <instance>/runtime.yaml and the package strategy.yaml catalog — never invent a number. Three things, plain language, no raw YAML:

  • Cadence — how often it acts. From the external_scanner's interval_seconds. If inputs carry a slower decision clock (recalibrationHours, thesisRefreshHours, regimeRefreshHours), lead with THAT and note the wake interval. Translate to human: interval_seconds: 300 → "scans every 5 minutes"; interval_seconds: 21600 + recalibrationHours: 168 → "re-reads the whole market weekly, waking every 6h to act on that read."

  • Scoring — what it grades and the entry bar. One or two sentences: the catalog belief_plain/thesis (what signal it scores) + the runtime inputs gate (minScore and the conviction bands, leverageTiers/marginPctTiers). e.g. "ranks the book by relative strength + smart-money lean; opens a name only above its score threshold, sizing bigger at higher conviction (leverage steps up base→apex)."

  • Protection — the DSL exit ladder. From exit.dsl_preset: the hard stop (phase1.max_loss_pct), the profit-lock ladder (phase2.tiers: first trigger_pct → top lock_hw_pct), and any time cut (weak_peak_cut/hard_timeout). State whether it has a manual close action or is DSL-only (no CLOSE_POSITION action → "no manual exits — the stop does all the selling"). e.g. "hard stop at −18% from entry; as a winner runs, a trailing floor ratchets up, locking profit from +8% to +80%; a stalled position is cut at 48h." Say every stop twice — as ROE and as price: the price distance is ROE ÷ leverage (a 15% ROE hard stop at 5× sits 3% from entry, at 2× 7.5%); quote both from the recipe, never a computed table — a stop table off by the leverage factor sells a leverage change the user did not understand.

  • Cap — how many entries a day. From risk.guard_rails.max_entries_per_day: say the number, and that once it is hit the runtime logs Runtime paused: Max Entries/Day and opens nothing until 00:00 UTC — its own rule, not a fault. While it holds, status.py shows the row as ⏸ paused (health stays ✅). Before raising marginPct, say max_entries_per_day × marginPct — the share of equity a day may commit (4 × 20% = 80%, where the venue starts warning); above ~60% say so and get a fresh yes, and never present the warning that follows as a market event.

  • Ownership — one line, the name first. "PurpleFrog's Starling is live — purplefrog-starling in your strategy list, saved at /data/workspace/strategies/purplefrog-starling/. Say 'widen the stop' any time; it applies in place — no close, no new wallet." Never "Starling is live" — the template's name is not theirs (the update path below).

Keep it to ~5 short lines per strategy. Multi-instance packages whose legs differ (e.g. a long book vs a short book, core vs ballast) get one block each or a shared block that names the per-side difference. This is what turns "it's live" into "here's exactly how it trades" — required even when the user didn't ask.

Monitor — what am I running? / is it actually live?

"What strategies am I running?" / "list my strategies" / "is my fleet healthy?" → Read the scanner fields by scheduleMode — the same value means different things. On interval, runCount climbs every tick (complete, skip and error alike; the signals it emitted are signalsProduced, a separate counter). On external — every signal scanner the catalog ships; position_tracker is interval — the scaffold POSTs on every tick, but an empty list updates the liveness clock and returns before ingest, so runCount climbs only on a tick that emits, initialized stays false until the first non-empty POST, and intervalSeconds: 0 / nextRunAt: null are the correct values rather than an unwired runtime. lastAliveAt is the tick. So runCount: 0 with a fresh lastAliveAt is a scanner finding nothing, not one that never ran — and the recipe's own interval_seconds is the scaffold's POST cadence, so a file saying 60 while the runtime reports 0 is not a contradiction either — never call a strategy dead without checking its ticks first: never a fault, never a reason to close and recreate (each new strategy wallet costs a real creation fee); the fix for "it isn't trading" is the gates or the budget, applied in place (below). A scanner interval under 60 s on a small book is fee churn — refuse it with the arithmetic (fills × fee against the budget), not a claim about timing. Decision tree: references/liveness-verification.md. python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/status.py (<id> filters, --fast skips the per-runtime health call, --json for machine output). It is the single source of truth — live strategy_list ∪ runtime list (the same runtime-CLI read senpi-portfolio also quotes — neither surface independently confirms the other), never the ephemeral deploy state — so don't hand-compose strategy_list. A strategy with no runtime is not "broken": it is just not autonomous, and status.py labels how it is managed (copy, manual, …) — never call it idle. The one real anomaly is an autonomous package strategy missing its runtime (no-runtime). Full label set: references/lifecycle.md. Two more facts ride every deep row, because health alone misleads on both: ⏸ paused — the runtime's OWN risk gate (components.risk) holds entries (daily cap, loss halt, cooldown), printed with the gate's reason and reset verbatim; health stays ✅, nothing is broken, and closing/redeploying resets nothing but the book. ✎ running recipe ≠ disk — the descriptor the runtime renders differs from the package on disk: an edit that was never applied → deploy.py update (below). When YOU call a trade tool (ratchet_stop_add, strategy_close, create_position…) it runs behind the user's trade-approval gate: an unapproved call times out, is denied, and returns an EMPTY result. Empty = not executed — say the approval timed out and nothing was placed; never report it as done, never re-issue it in a loop (the same rule senpi-trade's guardrail row carries — keep the two in sync).

"Are my open positions protected? / do they have a stop-loss?" → the DSL coverage verdict (PROTECTED / UNPROTECTED / STOP-NOT-ON-VENUE) — a separate read, not the runtime list above. Key trap: an unprotected position is an absence in senpi dsl positions — reconcile the tracked set: senpi-trading-runtime/references/dsl-protection-check.md.

Do not trust "runtime: running" alone. A strategy is live only when its runtime is running AND each instance's external_scanner has a recent successful tick. Confirm it on a read-only surface — every one of these is read-only; the money path is deploy.py create|runtime (see the funded path above) and nothing here is it:

  • python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/deploy.py verify <id> — the per-instance verdict over the surfaces below
  • python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/status.py <id> — the fleet view + each runtime's own health verdict
  • openclaw senpi deploy status / deploy.py status [<id>] — the agent's ONE last-deploy-job record
  • openclaw senpi scanner -r <runtime_id> — the scanner rows (lastRunStatus, runCount), the tick itself
  • openclaw senpi status -r <runtime_id> --json / state -r <runtime_id> --json; liveness decision tree → references/liveness-verification.md; DSL / action / position troubleshooting → openclaw senpi dsl|action … and senpi-trading-runtime/references/runtime-concepts.md

runtime_id = each instance's runtime.yaml top-level name (spider-swing); they all carry group: <id>, so openclaw senpi runtime list matching group == <id> rediscovers them ledger-free. Never schedule an agent-turn cron to watch a strategy — every openclaw cron firing is a full model call (a 5-minute job is ~288 a day), and the runtime already supervises the strategy at zero model cost; read it here on demand. A check-in the user asks for: at most once or twice a day, cost stated first, and a yes before creating it.

Close — stop → trigger → (agent polls)

python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/close.py spider      # stop runtime(s) + trigger strategy_close; re-run to poll
python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/close.py --all       # close EVERY open strategy (all packages) + delete runtimes
python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/close.py --strategy-id <id> | --address <wallet>   # a wallet with NO package at all

Per strategy: stop the runtime (if live) → trigger strategy_close (flattens all positions

  • closes the strategy, funds returned). strategy_close is async, so the script does not wait — it returns closing and hands polling to you: re-run close.py spider until it reports closed. Re-runs are idempotent. A subset = one run each, sequential, never backgrounded with & (lifecycle.md). --instance <name> scopes an instance (needs its live runtime to map; else omit to close all). Redeploy = openclaw senpi validate → close → create, in that order. Discovery is strategy-driven: close also cleans up an attributed package's orphaned (no-runtime) wallet, and --strategy-id/--address close one with no package at all (references/lifecycle.md).

Applying an edit to a strategy that is already LIVE

"Make my live strategy more aggressive." The edit is authored in senpi-strategy-author, never here. Read the runtime's own state first (status.py <id> / openclaw senpi status -r <runtime>): a Runtime paused: Max Entries/Day line or a gate that is not OPEN means nothing opens today at any balance — a top-up "for a fifth position" is the wrong advice; free margin for a perp book is the perps withdrawable, never spot-side USDC. The clearinghouse alone cannot say why nothing is opening. Re-running create (or senpi deploy) will NOT apply it — it is idempotent, so it adopts the existing wallet and leaves the deployed scanner as it is.

Apply it in place — openclaw senpi update. No close, no fresh wallet, no market exit; DSL state, scanner stores and action history survive. senpi validate <instance-dir> writes the proof --apply needs; python3 /data/.openclaw/skills/senpi-strategy-ops/scripts/deploy.py update <pkg> --id <runtime_id> PLANS (the structural preflight, then the verb; add --apply to commit — it stops if the box has no update verb yet: then STOP too, never close-and-redeploy). Read the plan out first: dsl_preset is forward-only — new entries only, never one already open (other exit: fields, e.g. order_type, DO reach open positions) — never let "tighter" be heard as "my open trades are tighter". A DSL change has two targets, and the user picks which: (a) future positions — the file, via update; (b) the positions open now — ratchet_stop_edit, one call and one approval per position; (c) both. Read both first (the file and ratchet_stop_list), show the drift and each position's new floor in numbers, then ask (a), (b) or (c) — never assume (a), never touch an open position without its own approval: references/editing-a-live-strategy.md. Call it an update to the user, never a "redeploy" — that word is the market-exit path below; an edit that closes nothing must never sound like one. Saved is not applied. update without --apply only plans — its first line reads Dry run for <runtime_id> — nothing has been applied. — so never pipe openclaw senpi output through tail/head. An edit is live only once --apply exits 0 and the running strategy shows the change (the two reads: references/editing-a-live-strategy.md); until then tell the user it is saved, not applied — never "done" or "live".

Only a changed strategy.wallet, a renamed or moved external scanner, or a changed action_type still need close-and-redeploy, which market-exits every open position and drops any custom ratchet ladder — take explicit consent in those words first. Everything else: references/editing-a-live-strategy.md.

Invariants

  • The wallet-creation MCP call carries attribution skillName/skillVersion = the package strategy.yaml id/version (not this skill's); deploy.py does it automatically.
  • The runtime package is @senpi-ai/runtime — never @senpi/runtime.

Install — include the MCP helper

The scripts import a vendored MCP helper, scripts/mcp_client.py, at runtime — install the whole scripts/ directory (omitting mcp_client.py fails with No module named 'mcp_client'). Stdlib only, no other runtime dependencies.

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

senpi-strategy-ops

Default branch

main

Latest commit

5545f72

Tree SHA

389d6be