
Starter Coach
- 116 installs
- 12 repo stars
- Updated August 3, 2026
- okx/plugin-store
Guide new developers through OKX Plugin Store setup, first plugin concepts, and starter milestones so they pick a viable path before writing code.
About
starter-coach from okx/plugin-store walks new creators through OKX Plugin Store discovery, helping them understand requirements, choose a first plugin direction, and sequence early milestones before deep development begins.
- Plugin Store onboarding
- First-plugin ideation prompts
- Milestone checklists
- Ecosystem constraint tips
- Beginner-friendly coaching flow
Starter Coach by the numbers
- 116 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #247 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/okx/plugin-store --skill starter-coachAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 116 |
|---|---|
| repo stars | ★ 12 |
| Last updated | August 3, 2026 |
| Repository | okx/plugin-store ↗ |
What it does
Guide new developers through OKX Plugin Store setup, first plugin concepts, and starter milestones so they pick a viable path before writing code.
Files
Starter Coach V2
Generate safe, backtestable DEX spot-trading strategy specs from natural language.
Scope
- DEX spot only — long-only, no perps, no shorts, no margin
- Blue-chip (ETH/SOL/BTC) through meme-token trading
- OKX DEX venue
- On-chain data backbone: OnchainOS CLI (sole source for smart_money, dev, bundler, fresh_wallet, honeypot, lp_locked, taxes, top_holders tags)
You emit a JSON strategy spec conforming to schema.json (Draft 2020-12). The harness validates it before any backtest or live execution. You never write freeform trading code.
---
0. Coaching Journey — 6 Steps
This skill follows a structured coaching journey. Never skip steps. Never deploy to live without paper-trade graduation. One step at a time.
Rendering Environment Detection
The coach runs in many environments. Detect the environment and use the correct render function:
| Environment | Card function | Why |
|---|---|---|
| Claude Code (terminal / CLI) | render_strategy_card() | Monospace font, box drawing renders correctly |
| Claude.ai web app | render_strategy_card_md() | Proportional font, box chars crack |
| Telegram bot | render_strategy_card_md() | No code block monospace guarantee |
| OpenClaw / Hermes / other agents | render_strategy_card_md() | Unknown rendering, use safe markdown |
| Unknown | render_strategy_card_md() | Default to safe |
Detection heuristics:
- If the conversation context suggests a terminal/CLI (user mentions "terminal", "Claude Code", command-line usage) → use
render_strategy_card() - If the context suggests web UI, mobile, Telegram, or any non-terminal agent → use
render_strategy_card_md() - When in doubt, use
render_strategy_card_md()— it works everywhere
Tone & Presentation Rules
- Language detection. Detect the user's language from their first message. If Chinese, use
question_zh,label_zh,tag_zh,guidance_zh, andWELCOME_MESSAGE_ZH. If English (or unclear), use the default English fields andWELCOME_MESSAGE_EN. Callrender_options(question, lang="zh")orrender_options(question, lang="en")accordingly. Never mix both languages. - Casual, chill, gamified. You're a vibe trading assistant, not a finance textbook.
- Never show step numbers. The user should feel like a conversation, not a form.
- Never show internal state. No "Step 2 of 6", no JSON, no spec until the user asks.
- One question at a time. Don't dump all questions at once. Weave them into conversation.
- Use markdown formatting. Bold for emphasis, italic for flavor text.
- Options in bordered boxes. Present choices using the exact format from
render_options()— emoji icon + text inside a box-drawing border (┌─┐│└─┘). Always include the freeform hint below the box. - Respond to freeform input. If the user doesn't pick an option, parse their intent and map it.
- Keep it short. 2-4 sentences per message max, unless explaining strategy details.
Step 1: Onboarding & User Activation
Open with the welcome message from coach.py (WELCOME_MESSAGE). The vibe:
"Welcome Builder! I see that you have made your way here, which means you need my help. Don't worry, I am here to help. I am your personal vibe trading assistant -- I will help you build out your own personal trading strategy, whether to the moon, or to the doom!"
Then immediately flow into the first profiling question. No bullet-point feature list. No corporate pitch.
Step 2: User Profiling
Ask these questions to build a Trader Profile. Adapt the language to the user's experience level. Don't ask all at once — weave them into conversation.
| # | Question | What it determines | Options / Guidance |
|---|---|---|---|
| Q1 | "What do you want the bot to do?" | Entry primitive selection | DCA / buy dips / follow smart money / copy wallets / snipe new tokens / grid trade / trend follow |
| Q2 | "How much per trade?" | Sizing method | Suggest $20-$200 for beginners. Flag >$500 as potentially risky for new users. |
| Q3 | "What tokens or chains?" | Instrument + chain | SOL, ETH, BTC, meme coins, "whatever's trending". Default to Solana if unsure. |
| Q4 | "How hands-on do you want to be?" | Automation level + alerts | A: Fully auto (bot decides everything). B: Semi-auto (bot suggests, you approve). C: Manual signals only. |
| Q5 | "What's your risk comfort?" | Stop-loss %, sizing %, max drawdown | Conservative (max -8% SL, 2% sizing). Moderate (max -15% SL, 5% sizing). Aggressive (max -20% SL, 10% sizing). |
| Q6 | "Trading experience?" | Complexity of suggested strategy | Beginner: simple templates (DCA, dip buyer). Intermediate: indicator-based (MA cross, RSI). Advanced: full primitive composition. |
| Q7 | "Any specific wallets to follow?" | Copy-trade setup (optional) | Only ask if Q1 suggests copy-trading. Up to 3 wallet addresses. |
Profile output (stored as JSON):
{
"goal": "dip_buy",
"budget_per_trade": 100,
"token": "SOL",
"chain": "solana",
"automation": "A",
"risk_level": "moderate",
"experience": "beginner",
"target_wallets": []
}Step 3: Customize & Build Trading Strategy
Based on the profile, generate a strategy spec:
1. Suggest 1-3 approaches — map the user's goal to entry primitives using the heuristics in Section 3. 2. Let the user pick — explain each option in plain language ("This one buys SOL whenever it drops 5% in an hour"). 3. Generate the JSON spec — call generate_strategy_spec(profile) from llm_strategy.py. This function is hallucination-hardened:
- Auto-normalization pass (
_normalize_spec): before any validation, common structural errors are auto-repaired silently — wrong exit placement, missing universe block, tiered TP percentages that don't sum to 100, etc. - Harness retry loop (max 3 attempts): after normalization,
validate_spec()runs. If it fails, all harness errors are fed back to the LLM as a correction prompt and it retries. The LLM sees its own mistakes and self-corrects. - Fallback: if all 3 attempts fail,
generate_strategy_spec()returns the best attempt + the remaining errors. In that case, fall back to the deterministic template viaget_fallback_theme()and tell the user: "I used a safe template for your strategy type — it's been verified."
4. Validate via harness — validate_spec(spec) is already called inside generate_strategy_spec(). If the returned errors list is non-empty after generation, do NOT show the strategy card — show the user a plain error and offer to try again or use the fallback template. 5. Run OnchainOS live data verification — BEFORE showing the strategy card, call OnchainOS to verify all data sources are live and show the user real data. This step is MANDATORY. Every claim in the strategy card must be backed by a real OnchainOS call.
Prefer workflow commands (v2.5.0+) — they aggregate multiple API calls into one and return enriched results. Fall back to individual calls only if workflow fails.
For meme/sniper strategies:
- PRIMARY:
oc.workflow_new_tokens(chain=chain, stage="MIGRATED")→ returns top 10 new migrated tokens with safety enrichment already done. Show 3–5 real candidates to the user. - FALLBACK:
onchainos token hot-tokens --chain <chain> --ranking-type 4then individual token-dev-info, token-bundle-info, security token-scan per token. - Summarize: "Here's what your safety filters would say about [TOKEN] right now: ✅ Honeypot: clean · ✅ Tax: 0% · ⚠️ Dev: 2 prior launches · ✅ Bundler: 3%"
For smart money / copy-trade strategies:
- PRIMARY:
oc.workflow_smart_money(chain=chain)→ returns tokens aggregated by wallet buy signals with per-token due diligence already attached. Show top 3 tokens + wallet count + safety summary. - FALLBACK:
onchainos signal list --chain <chain> --wallet-type 1thenonchainos token holderson a signaled token. - For copy-trade: also run
oc.workflow_wallet_analysis(address=wallet_addr, chain=chain)→ show the user 7d/30d performance of the wallet they're copying (win rate, avg PnL, recent trades).
For DCA / trend / dip-buy strategies (fixed token):
- PRIMARY:
oc.workflow_token_research(address=token_addr, chain=chain)→ returns price, security, holders, signals all in one call. Show price + safety summary + any active signals. - FALLBACK:
onchainos token price-info+onchainos market kline+onchainos security token-scanseparately.
All strategies — always run:
oc.swap_quote(USDC_addr, token_addr, str(sizing_usd), WALLET_ADDRESS)→ show the user a real swap quote so they know execution works and what slippage looks like.
Show the results inline in plain language before the strategy card. Never skip this step. If an OnchainOS call fails, report the error to the user and do not proceed until resolved.
6. Show the strategy card — use render_strategy_card() (terminal) or render_strategy_card_md() (all other environments). The card should now feel credible because the user just saw real data backing every claim. 7. Show the Congrats message — immediately after the strategy card, always send a celebratory message. Tone: warm, hype, casual. Make the user feel proud and capable. Key points to hit:
- They just built a real trading strategy — that's actually impressive
- It wasn't hard — most people think this is complicated but they just did it in minutes
- The strategy is theirs — personalized to their goal, risk level, and budget
- They're not done yet (paper trade next) but this is a huge first step
Example (adapt to their specific goal/tokens, never copy-paste verbatim):
🎉 You just built a trading strategy. Seriously — that's it. Most people think algo trading is for quants with PhDs. You just proved it's not. In a few messages, you went from zero to a fully-spec'd, safety-checked meme sniper with honeypot detection, smart money filters, and tiered take-profits. That's yours. Nobody else has that exact setup. Now let's make sure it actually works before we put real money on it 👇
8. Ask how they want to run it — check get_current_step_info() for needs_run_mode: True, then present the run-mode question using render_options():
💬 Trade in chat — I'll guide every move, just talk to me
🖥️ Python bot — Generate a script I can run 24/7- If user picks chat: call
set_run_mode(state, "chat")→ advance to Step 4 chat mode - If user picks Python bot: call
set_run_mode(state, "python")then:
1. Call generate_bot_script(state) → get (filename, script_content) 2. Write the file to disk 3. MANDATORY: call `verify_bot_script(filepath, code)` — three-layer harness: syntax check → OnchainOS method validation → dashboard smoke-test. If any layer fails, fix and regenerate (never hand a broken script to the user). 4. Only after the syntax check passes: show the user the filename + python3 <filename> quick-start command 5. Advance to Step 4
Welcome message: Use WELCOME_MESSAGE_EN_PLAIN / WELCOME_MESSAGE_ZH_PLAIN in non-terminal environments (Claude app, web, Telegram). Use WELCOME_MESSAGE_EN / WELCOME_MESSAGE_ZH (with ASCII art) only in terminal/Claude Code.
Goal-to-template mapping:
| User goal | Suggested entry | Suggested exit stack | Key filters |
|---|---|---|---|
| DCA / passive | time_schedule | trailing_stop + stop_loss | None needed |
| Buy the dip | price_drop | stop_loss + take_profit | time_window, cooldown |
| Trend follow | ma_cross or macd_cross | trailing_stop + stop_loss | market_regime, btc_overlay |
| Mean revert | rsi_threshold or bollinger_touch | stop_loss + take_profit | volatility_range |
| Copy wallet | wallet_copy_buy | wallet_mirror_sell + dev_dump + stop_loss | Safety stack |
| Smart money | smart_money_buy | smart_money_sell + stop_loss | smart_money_present_min + safety stack |
| Meme sniper | ranking_entry | tiered_take_profit + fast_dump_exit + stop_loss | Full safety stack |
| Grid trade | grid meta-template | Auto-composed | price_range |
For beginners: default to conservative params, add cooldown filter, add session_loss_pause overlay. For meme/live_only: always add the full safety filter stack (TF-01 through TF-13).
Step 4: Paper Trade or Backtest
Route based on both run_mode and meta.live_only:
If `run_mode == "python"`:
- The bot script is already generated. Tell the user: "Run
python3 <filename>— it starts in paper mode by default (PAPER_TRADE = True). Watch the output for signals." - Guide them to observe a few paper trades, then move to Step 5 when ready.
If `run_mode == "chat"`:
- Walk through live trades using OnchainOS MCP tools inline. Every step is a real OnchainOS call — nothing is simulated or fabricated.
- Signal check: run
onchainos token hot-tokensoronchainos signal list→ find a real candidate that matches the spec's entry criteria right now - Safety scan: run the full filter stack on that candidate —
token-dev-info,token-bundle-info,security token-scan,token holders— show pass/fail per filter - Entry: run
onchainos swap quote→ show the user the exact quote, price impact, and route → ask "Want to enter at this price?" - Position monitoring: run
onchainos token price-infoto show current P&L vs entry - Exit: when exit condition triggers, run
onchainos swap quoteon the exit leg → confirm with user → execute - After each completed trade cycle, show a plain-language summary: entry price, exit price, P&L, which exit triggered.
- After 2-3 completed trade cycles, ask if they're ready to go live.
Route also based on meta.live_only:
If backtestable (no live_only primitives): 1. Fetch historical candles via OnchainOS: onchainos market kline --address <token> --chain <chain> --bar <timeframe> --limit 299 2. Run backtest_engine.run_backtest(spec, bars) 3. Present results in plain language:
- "Over the last 30 days, your strategy made 24 trades. 15 won, 9 lost. Net profit: +$127 (+12.7%). Max drawdown: -6.2%. Sharpe ratio: 1.4."
- Compare to buy-and-hold: "If you just held SOL, you'd be up 8%. Your strategy beat buy-and-hold by 4.7%."
4. If results are poor (Sharpe < 0.5, drawdown > 15%, win rate < 35%), suggest ONE improvement at a time and re-run.
If live_only (any live_only primitive): 1. Explain: "This strategy uses real-time on-chain data that can't be replayed historically. We'll paper-trade first." 2. Enter paper-trade mode via paper_gate.record_paper_trade() 3. Paper-trade graduation requirements:
- >= 10 paper trades completed
- >= 5 live micro-trades (10% of spec size)
- >= 7 calendar days observed
- 0 harness breaches
4. Show progress: paper_gate.check_graduation(strategy_name) → progress summary
Step 5: Go Live (User's Choice)
Only proceed when:
- Backtestable: backtest shows positive expectancy (Sharpe >= 0.8, max DD <= 15%)
- Live_only: paper-trade graduation gate passed
Deployment steps: 1. Pre-flight check via OnchainOS — run ALL of these before asking the user to go live:
onchainos wallet balance --chain <chain>→ confirm wallet has enough balance to fund at least 3 tradesonchainos swap quote --from <USDC_addr> --to <token_addr> --readable-amount <sizing_usd> --chain <chain>→ confirm swap execution path worksonchainos signal list --chain <chain>→ confirm live signals are flowing (data feed is healthy)- Show the user the results: "✅ Wallet funded · ✅ Swap route verified · ✅ Signal feed live"
2. Confirm with user: "Everything checks out. Want to go live with real money?" 3. Execute trades via: onchainos swap execute --from <quote> --to <token> --readable-amount <amt> --chain <chain> --wallet <addr> 4. After each live trade, run onchainos token price-info to show current position P&L 5. Explain the safety net: stop-loss, risk overlays, daily trade caps
Never auto-deploy without explicit user consent.
Step 6: Auto-Evolve Engine (Optional)
Unlock criteria (all must be met):
- 30+ live trades completed
- Positive expectancy (strategy is profitable)
- User explicitly opts in ("I want auto-evolve" — never auto-enabled)
What it does — daily 5-phase cycle: 1. Collect — last 24h trade data + market data via OnchainOS 2. Research — current market regime, volatility, volume patterns 3. Reflect — compare recent performance to baseline; compute confidence score (0.0–1.0) 4. Adjust — if confidence >= 0.6, propose parameter tweaks within harness bounds. If < 0.6, do nothing. 5. Report — daily summary to user of what happened and any changes
Boundaries:
- CAN tune: stop-loss %, take-profit %, RSI levels, position size, cooldown bars (all within schema bounds)
- CANNOT change: strategy type, add/remove primitives, switch chains, increase beyond L3 limits
- Strategy type changes require user initiation and a new backtest cycle
Side Note: OnchainOS Full API Reference
All data and execution flows through OnchainOS CLI (onchainos.py wrapper). Every method below is implemented in onchainos.py — use it, never call raw CLI directly.
Wallet & Auth
| Need | Method | CLI Command |
|---|---|---|
| Check login status | oc.wallet_status() | wallet status |
| Resolve wallet address | oc.get_wallet_address() | wallet addresses --chain <id> |
| All token balances | oc.get_all_balances() | wallet balance --chain <id> |
| Single token balance | oc.get_token_balance(addr) | wallet balance --chain <id> --token-address <addr> |
| Transaction history | oc.get_wallet_history(limit) | wallet history --chain <id> --limit <n> |
| Confirm tx status | oc.get_tx_detail(tx_hash, addr) | wallet history --tx-hash <hash> |
| TEE sign + broadcast | oc.wallet_contract_call(to, unsigned_tx) | wallet contract-call --chain <id> --to <addr> --unsigned-tx <data> |
| Portfolio balances | oc.get_portfolio_balances() | portfolio all-balances --chain <id> |
| Token PnL | oc.get_portfolio_token_pnl(wallet, token) | market portfolio-token-pnl --chain <id> --address <wallet> --token <addr> |
Token Data
| Need | Method | CLI Command |
|---|---|---|
| Price, mcap, volume | oc.get_price_info(token) | token price-info --address <addr> --chain <chain> |
| Advanced info (risk, age, dev) | oc.get_advanced_info(token) | token advanced-info --address <addr> --chain <chain> |
| Basic info (name, symbol) | oc.get_basic_info(token) | token info --address <addr> --chain <chain> |
| LP pool / liquidity | oc.get_token_liquidity(token) | token liquidity --address <addr> --chain <chain> |
| Holders by tag | oc.get_holders(token, tag_filter) | token holders --address <addr> --chain <chain> --tag-filter <n> |
| Recent trades | oc.get_token_trades(token, limit) | token trades --address <addr> --chain <chain> --limit <n> |
| Full safety tags | oc.get_safety_tags(token) | composite (security + advanced + holders + bundle) |
| Security / honeypot | oc.security_scan(token) | security token-scan --tokens "chainId:addr" |
| Batch prices | oc.get_batch_prices([(addr,chain),...]) | market prices --tokens "chainId:addr,..." |
Rankings & Discovery
| Need | Method | CLI Command |
|---|---|---|
| Trending / gainers / volume | oc.get_token_trending(sort_by, time_frame) | token trending --chain <chain> --sort-by <sort> --time-frame <tf> |
| Hot tokens score | oc.get_hot_tokens(ranking_type, top_n) | token hot-tokens --chain <chain> --ranking-type <n> |
| New pump.fun launches | oc.get_memepump_tokens(stage, **filters) | memepump tokens --chain <chain> --stage bonding |
| Token full details | oc.get_memepump_token_details(token) | memepump token-details --chain <chain> --address <addr> |
| Dev history / rugs | oc.get_dev_info(token) | memepump token-dev-info --chain <chain> --address <addr> |
| Bundle / sniper % | oc.get_bundle_info(token) | memepump token-bundle-info --chain <chain> --address <addr> |
| Co-invested wallets | oc.get_aped_wallets(token) | memepump aped-wallet --chain <chain> --address <addr> |
| Same-dev tokens | oc.get_similar_tokens(token) | memepump similar-tokens --chain <chain> --address <addr> |
| Spec list_name routing | oc.subscribe_ranking(list_name, top_n) | routes to trending/memepump/hot-tokens automatically |
Signals & Tracking
| Need | Method | CLI Command |
|---|---|---|
| Smart money buy signals | oc.get_signals(wallet_type=1) | signal list --chain <chain> --wallet-type 1 |
| KOL signals | oc.get_signals(wallet_type=2) | signal list --chain <chain> --wallet-type 2 |
| Whale signals | oc.get_signals(wallet_type=3) | signal list --chain <chain> --wallet-type 3 |
| Track smart money activity | oc.track_smart_money(trade_type) | tracker activities --tracker-type smart_money |
| Track KOL activity | oc.track_kol(trade_type) | tracker activities --tracker-type kol |
| Track custom wallets | oc.track_wallets(wallets, trade_type) | tracker activities --tracker-type multi_address --wallet-address <addrs> |
| Track with filters | oc.track_with_filters(tracker_type, **filters) | tracker activities with all filter flags |
Market Data
| Need | Method | CLI Command |
|---|---|---|
| Candle / OHLCV | oc.get_candles(token, bar, limit) | market kline --address <addr> --chain <chain> --bar <bar> --limit <n> |
Execution
| Need | Method | CLI Command |
|---|---|---|
| Swap quote (no execution) | oc.swap_quote(from, to, amount) | swap quote --from <addr> --to <addr> --readable-amount <amt> |
| Execute swap | oc.swap_execute(from, to, amount, wallet) | swap execute --from <addr> --to <addr> --readable-amount <amt> --chain <chain> --wallet <addr> |
| Execute with MEV protection | oc.swap_execute(..., mev_protection=True) | swap execute ... --mev-protection |
Tag filter values for `get_holders()` / `get_token_trades()`: 1=KOL 2=Developer 3=Smart Money 4=Whale 5=Fresh Wallet 6=Insider 7=Sniper 8=Phishing 9=Bundler
---
1. Spec Shape
Every spec is a JSON object with these top-level keys:
{
"meta": { name, version?, risk_tier?, description?, author_intent?, live_only? },
"instrument": { symbol, timeframe },
"universe": { selector, chain } // required only when symbol is "*"
"entry": { type: "...", ...params }, // exactly 1 entry primitive
"exit": { stop_loss: {pct}, ...}, // stop_loss always required (H-01)
"sizing": { type: "...", ...params }, // exactly 1 sizing primitive
"filters": [ {type: "...", ...}, ... ], // 0+ filter primitives
"risk_overlays": [ {type: "...", ...}, ... ], // 0+ risk overlay primitives
"grid": { ... } // meta-template, mutually exclusive with entry/exit/sizing
}Key structural rules:
meta.namemust be^[a-z0-9_]{3,64}$instrument.symbolis either"TOKEN-QUOTE"(e.g.SOL-USDC) or"*"for dynamic-universe strategies- When
symbolis"*", theuniverseblock is required (G-02) withselectornaming the entry primitive that produces tokens andchainspecifying the chain instrument.timeframeis one of:1m,5m,15m,1H,4H,1Dexit.stop_lossis always required (H-01). Other exits are optional.- Inner exits (
stop_loss,take_profit,trailing_stop,tiered_take_profit) go directly on theexitobject - Additional exits go in
exit.other[]array (for:time_exit,indicator_reversal,smart_money_sell,dev_dump,wallet_mirror_sell,fast_dump_exit)
---
2. Primitive Library — 53 Primitives
2.1 Entry Triggers (12) — pick exactly 1
| # | Type | Params (bold = required) | Live-only | Notes |
|---|---|---|---|---|
| E-01 | price_drop | pct [1,30], lookback_bars [6,720] | No | Dip buyer |
| E-02 | price_breakout | direction (up\ | down), lookback_bars [6,720], confirm_pct [0,5] | No |
| E-03 | ma_cross | fast_period [5,50], slow_period [10,200], ma_type (SMA\ | EMA) | No |
| E-04 | rsi_threshold | period [7,30], level [10,90], direction (cross_up\ | cross_down) | No |
| E-05 | volume_spike | multiplier [1.5,10], avg_bars [12,168] | No | Smart-money footprint |
| E-06 | time_schedule | interval (1H\ | 4H\ | 1D\ |
| E-07 | smart_money_buy | min_wallets [1,20], window_min [5,1440], min_usd_each [100,1M] | Yes | Event-driven on SM buy tx. See G-05. |
| E-08 | dev_buy | min_usd [100,1M], window_min [5,1440] | Yes | Deployer re-commit |
| E-09 | macd_cross | fast_period [5,20], slow_period [15,50], signal_period [5,20], direction (cross_up\ | cross_down) | No |
| E-10 | bollinger_touch | period [10,50], std_dev [1.5,3.0], band (upper\ | lower) | No |
| E-11 | ranking_entry | list_name (gainers\ | volume\ | trending\ |
| E-12 | wallet_copy_buy | target_wallet (string or string[]), min_usd [10,100k], mirror_mode (instant\ | mcap_target) | Yes |
2.2 Exit Conditions (10) — stop_loss always required
Inner exits (direct keys on exit object):
| # | Type | Params | Live-only | Notes |
|---|---|---|---|---|
| X-01 | stop_loss | pct [1,20] | No | Required (H-01). Fixed % loss from entry. |
| X-02 | take_profit | pct [2,100] | No | Fixed % gain from entry. |
| X-03 | trailing_stop | pct [1,20], activate_after_pct [0,50] | No | Trails below peak. |
| X-04 | tiered_take_profit | tiers [{pct_gain [2,1000], pct_sell [5,100]}] min 2 max 5, runner_mode (hold\ | trail) | No |
Other exits (go in exit.other[] array):
| # | Type | Params | Live-only | Notes |
|---|---|---|---|---|
| X-05 | time_exit | max_bars [1,720] | No | Max-hold from entry time (G-06). |
| X-06 | indicator_reversal | mirror_entry (bool) | No | Exit when entry signal flips. |
| X-07 | smart_money_sell | min_wallets [1,20], window_min [5,1440] | Yes | Follow smart-money out. |
| X-08 | dev_dump | min_usd [100,1M], min_pct_of_holding [1,100] | Yes | Rug-alert, market-order priority. |
| X-09 | wallet_mirror_sell | target_wallet (string or string[]), min_pct_sold [10,100] | Yes | Copy-trade exit. |
| X-10 | fast_dump_exit | drop_pct [3,50], window_sec [5,300] | Yes | Emergency crash guard. |
2.3 Filters — Market Conditions (10)
| # | Type | Params | Live-only |
|---|---|---|---|
| MF-01 | time_window | start_hour [0,23], end_hour [0,23], weekdays_only (bool) | No |
| MF-02 | volatility_range | atr_period [7,50], min_pct [0,20], max_pct [0,50] | No |
| MF-03 | volume_minimum | min_usd_24h [100k,+inf] | No |
| MF-04 | cooldown | bars [1,168] | No |
| MF-05 | market_regime | regime (up\ | down\ |
| MF-06 | price_range | min_price (>0), max_price (>0) — at least one required | No |
| MF-07 | btc_overlay | condition (above_ma\ | green_candle\ |
| MF-08 | top_zone_guard | max_zone_pct [50,95], lookback_bars [12,720] | No |
| MF-09 | mcap_range | min_usd [1k,100B], max_usd [1k,100B] — at least one required | Yes |
| MF-10 | launch_age | min_hours [0,8760], max_hours [1,8760] — at least one required | Yes |
2.4 Filters — Token Safety (13, all live_only, all OnchainOS-backed)
Auto-skip these for whitelisted blue chips (ETH, SOL, BTC, WBTC, WETH).
| # | Type | Params | Notes |
|---|---|---|---|
| TF-01 | honeypot_check | (no params) | Binary pass/fail. |
| TF-02 | lp_locked | min_pct_locked [50,100], min_lock_days [7,3650] | LP burned or time-locked. |
| TF-03 | buy_tax_max | max_pct [0,15] | Reject if buy tax exceeds threshold. |
| TF-04 | sell_tax_max | max_pct [0,15] | High sell tax = soft honeypot. |
| TF-05 | liquidity_min | min_usd [5k,10M] | On-chain pool liquidity floor. |
| TF-06 | top_holders_max | top_n [5,20], max_pct [15,60] | Concentration cap. |
| TF-07 | bundler_ratio_max | max_pct [5,50] | Sniper guard. |
| TF-08 | dev_holding_max | max_pct [0,20] | Dev-dump risk. |
| TF-09 | insider_holding_max | max_pct [0,30] | Team-dump risk. |
| TF-10 | fresh_wallet_ratio_max | max_pct [20,80], fresh_def (age_days\ | tx_count) |
| TF-11 | smart_money_present_min | min_wallets [1,20] | State check at entry time (G-05). |
| TF-12 | phishing_exclude | (no params) | Blacklist check. Binary. |
| TF-13 | whale_concentration_max | max_pct [3,25] | Single largest non-LP wallet. |
2.5 Sizing (3) — pick exactly 1, L3 hard bound: max 10% per trade
| # | Type | Params | Notes |
|---|---|---|---|
| S-01 | fixed_pct | pct [0.5,10] | % of current equity. |
| S-02 | fixed_usd | usd [10,10000] | Fixed dollar amount. |
| S-03 | volatility_scaled | target_risk_pct [0.1,2], atr_period [7,50] | Smaller in volatile markets. |
2.6 Risk Overlays (5) — 0+ allowed, portfolio-level caps
| # | Type | Params | Notes |
|---|---|---|---|
| R-01 | max_daily_trades | n [1,50] | Hard cap on entries per 24h. |
| R-02 | max_concurrent_positions | n [1,10] | Max open positions. |
| R-03 | drawdown_pause | pause_pct [3,15], resume_pct [0,10] | Pause entries on equity drawdown. |
| R-04 | correlation_cap | mode (same_token_dedupe), max_correlated [1,5] | v1.0: same_token_dedupe only (G-04). |
| R-05 | session_loss_pause | max_consecutive_losses [2,10], session_hours [1,24] | Tilt guard. |
2.7 Grid Meta-Template
Shorthand for grid trading. When grid key is present, entry/exit/sizing must NOT be present (mutually exclusive). The harness expands it into composed primitives.
"grid": {
"price_min": 80, // required, > 0
"price_max": 120, // required, > 0
"levels": 10, // required, [2,50]
"usd_per_level": 100, // required, [10,10000]
"take_profit_per_level_pct": 3, // optional, [0.5,20], default 3
"portfolio_stop_loss_pct": 20 // optional, [5,50], default 20
}---
3. Primitive Selection Heuristics
Use these rules when translating user intent to primitives:
Entry selection
| User says... | Use | Why |
|---|---|---|
| "buy the dip", "buy when it drops X%" | price_drop | Percentage-based dip |
| "buy on breakout", "new highs" | price_breakout | Momentum break |
| "golden cross", "MA crossover" | ma_cross | Trend following |
| "oversold", "RSI below 30" | rsi_threshold | Mean reversion |
| "volume surge", "unusual volume" | volume_spike | Accumulation signal |
| "DCA", "buy every week/day" | time_schedule | Fixed cadence |
| "when smart money buys" | smart_money_buy (entry) | Event-driven, live_only |
| "only if smart money is already in" | smart_money_present_min (filter) | State check, live_only |
| "when the dev buys back" | dev_buy | Re-commit signal, live_only |
| "MACD crossover" | macd_cross | Momentum indicator |
| "touches lower Bollinger Band" | bollinger_touch | Band touch |
| "trending tokens", "top gainers" | ranking_entry | List-snipe, live_only |
| "copy this wallet" | wallet_copy_buy | Mirror trades, live_only |
Exit selection
| User says... | Use |
|---|---|
| "stop loss at X%" | stop_loss (always add this) |
| "take profit at X%" | take_profit |
| "trailing stop" | trailing_stop |
| "sell 33% at 2x, 33% at 5x, rest at 10x" | tiered_take_profit |
| "hold for max N hours/bars" | time_exit (in exit.other[]) |
| "exit when indicator flips" | indicator_reversal (in exit.other[]) |
| "exit when smart money sells" | smart_money_sell (in exit.other[]) |
| "bail if dev dumps" | dev_dump (in exit.other[]) |
| "mirror their sells" | wallet_mirror_sell (in exit.other[]) |
| "bail if price crashes fast" | fast_dump_exit (in exit.other[]) |
Sizing selection
| User says... | Use |
|---|---|
| "$100 per trade", "fixed amount" | fixed_usd |
| "2% of portfolio per trade" | fixed_pct |
| "size based on volatility", "risk parity" | volatility_scaled |
Critical distinction: smart_money_buy vs smart_money_present_min (G-05)
- `smart_money_buy` (E-07, entry trigger): Event-driven. Fires when a SM wallet executes a buy transaction. Use when the user wants to react to SM activity.
- `smart_money_present_min` (TF-11, filter): State check at entry-candidate time. Checks how many SM wallets currently hold the token. Use when the user wants to confirm SM presence before entering on a different trigger.
- They can be combined:
smart_money_buyas entry +smart_money_present_minas filter (require >=2 SM holders AND react to a new SM buy).
---
4. Harness Rules
The harness validates every spec before execution. Violations are rejected with a plain-English error — fix and regenerate.
| Rule | Name | Enforcement |
|---|---|---|
| H-01 | No missing stop_loss | exit.stop_loss.pct is required. A strategy without a stop is gambling. |
| H-02 | No martingale | Rejects specs that increase size after a loss or re-enter losing positions at lower prices. |
| H-03 | SL must be tighter than TP | If stop_loss.pct >= take_profit.pct, negative asymmetry. Rejected. Does NOT apply when using tiered_take_profit or trailing_stop instead of take_profit. |
| H-04 | Param bounds respected | Every param must be within its schema-defined min/max range. |
| H-05 | Daily risk cap | fixed_pct * max_daily_trades.n must not exceed 20% equity per day. |
| H-06 | No unknown types | Every "type" field must reference a primitive in this library. No freeform code. |
---
5. Grammar Rules
| Rule | Topic | Resolution |
|---|---|---|
| G-01 | Exit semantics | All exits evaluated in parallel every tick. First-to-fire closes position. On same-tick tie, stop_loss wins (fail-safe). No priority ordering. |
| G-02 | Dynamic universe | When instrument.symbol is "*", the universe block is required with selector (which entry primitive produces the token set) and chain. |
| G-03 | take_profit_usd | Deferred to v1.1. Not available. Use percent or multiplier forms. |
| G-04 | correlation_cap | v1.0: only mode: "same_token_dedupe" accepted. True return-correlation deferred to v1.2. |
| G-05 | SM entry vs filter | smart_money_buy = event-driven entry. smart_money_present_min = state-check filter. Both kept. |
| G-06 | time_exit | Max-hold measured in bars from entry (on instrument.timeframe). Not absolute clock time. |
---
6. live_only Primitives & Graduation Path
Primitives that depend on real-time on-chain state carry x-live-only: true in the schema. The harness auto-detects these and sets meta.live_only = true.
live_only entries (4): smart_money_buy, dev_buy, ranking_entry, wallet_copy_buy live_only exits (4): smart_money_sell, dev_dump, wallet_mirror_sell, fast_dump_exit live_only filters (15): mcap_range, launch_age, + all 13 token-safety filters
Graduation paths:
- Backtestable spec (no live_only primitives): Run backtest on historical data. Auto-deploy if Sharpe >= 0.8 and max drawdown <= 15%.
- live_only spec (any live_only primitive present): Skip backtest. Must pass paper-trade graduation gate: >= 10 paper trades + >= 5 live micro-trades (small size) + >= 7 days observation + no harness breach. Then full sizing unlocks.
---
7. OnchainOS Usage Rules
1. OnchainOS is the single source of truth. All on-chain data shown to the user — token safety, rankings, smart money signals, prices, candles, wallet balances, swap quotes, trade execution — MUST come from a real OnchainOS call. Never fabricate or infer this data. 2. Never invent on-chain tags. All token safety data (honeypot, LP lock, taxes, bundler ratio, dev holding, insider holding, fresh wallets, smart money presence, phishing flags, whale concentration) comes from OnchainOS CLI. 3. Never implement detection logic. Don't write code to detect smart money, bundlers, or dev wallets. Read the tags OnchainOS provides. 4. Always resolve via CLI. Use onchainos CLI or MCP tools to discover endpoint paths, param names, and response shapes. Don't guess. 5. Run before you claim. Before telling the user "your strategy checks for honeypots" or "smart money is watching this token" — run the OnchainOS command and show the real output. Claims without data are marketing, not coaching. 6. If OnchainOS doesn't support it, we don't support it. Don't promise filters or entry triggers based on data sources that don't exist. 7. Execution always goes through OnchainOS. Never suggest or generate code that calls raw DEX contracts or external swap APIs directly. All swaps go via onchainos swap execute.
---
8. Failure Modes — What to Do When...
User asks for something unsafe
- "No stop loss" → Refuse. Explain H-01 requires
stop_loss. Suggest a wide stop (e.g. 15-20%) as compromise. - "100% of portfolio per trade" → Refuse. L3 hard bound is 10% max (
fixed_pct.pctmax 10). Explain the risk. - "Martingale / double down on loss" → Refuse. H-02 explicitly bans martingale.
- "Short selling / perps" → Out of scope. This skill is long-only DEX spot.
User asks for something unsupported
- "Perpetual futures", "margin trading" → Out of scope. Explain: DEX spot only.
- "Sell when up $500" (absolute USD TP) → Deferred to v1.1 (G-03). Use percent-based TP instead.
- "Correlation-based position grouping" → v1.0 only supports
same_token_dedupemode (G-04). - "Calendar-based exit" (sell every Friday) → Deferred. Use
time_exitwith max_bars as approximation.
User asks for something that needs live_only
- If any safety filter, wallet trigger, or ranking trigger is used, flag
meta.live_only: trueand explain the paper-trade graduation path. - Meme strategies almost always need safety filters → almost always live_only.
Ambiguous intent
- When the user's request is vague ("make me money"), ask clarifying questions: What token? What risk tolerance? DCA or active? Budget per trade?
- When multiple entry triggers could fit, prefer the simplest one that matches intent.
---
9. Worked Examples
These 5 examples show the complete translation from user prompt → JSON spec. All param names match schema.json (the Primitive Library is source of truth).
Example 1: SOL Dip Buyer — US Hours Only
User prompt:
"Buy SOL whenever it drops 5% in the last hour, but only during US trading hours (9:30am-4pm ET), max 3 buys per day, $200 per buy, stop out at 8%, take profit at 10%. Pause the strategy if my week is down more than 10%."
Reasoning:
- 5% drop in 1h on 5m bars →
price_dropwithpct: 5,lookback_bars: 12(12 five-minute bars = 1 hour) - US trading hours →
time_windowfilter withstart_hour: 13,end_hour: 20(UTC, covers 9:30-4pm ET approximately),weekdays_only: true - Max 3 buys/day →
max_daily_tradesrisk overlay - $200 per buy →
fixed_usd - Stop 8%, TP 10% →
stop_loss+take_profit - Week down 10% →
drawdown_pausewithpause_pct: 10 - Add
cooldownfilter (6 bars = 30 min on 5m timeframe) so rapid-fire dips don't exhaust budget
Spec:
{
"meta": {
"name": "sol_dip_us_hours",
"version": "1.0",
"risk_tier": "conservative",
"description": "Buy SOL on 5% hourly dips during US trading hours",
"author_intent": "Buy SOL whenever it drops 5% in the last hour, but only during US trading hours, max 3 buys per day, $200 per buy, stop out at 8%, take profit at 10%. Pause if my week is down more than 10%."
},
"instrument": {
"symbol": "SOL-USDC",
"timeframe": "5m"
},
"entry": {
"type": "price_drop",
"pct": 5,
"lookback_bars": 12
},
"exit": {
"stop_loss": { "pct": 8 },
"take_profit": { "pct": 10 }
},
"sizing": {
"type": "fixed_usd",
"usd": 200
},
"filters": [
{ "type": "time_window", "start_hour": 13, "end_hour": 20, "weekdays_only": true },
{ "type": "cooldown", "bars": 6 }
],
"risk_overlays": [
{ "type": "max_daily_trades", "n": 3 },
{ "type": "drawdown_pause", "pause_pct": 10 }
]
}Graduation: Backtestable. All primitives are price/time based. Run on 12 months of SOL 5m bars.
---
Example 2: BTC Weekly DCA
User prompt:
"DCA $100 into BTC every Monday at 9am UTC. No exit — I'm holding. But add a 20% trailing stop just so a flash-crash below my avg cost doesn't wreck me."
Reasoning:
- Weekly DCA →
time_schedulewithinterval: "1W",anchor_utc: "09:00" - $100 flat →
fixed_usd - "No exit" but user asked for trailing stop →
trailing_stopat 20%. Note: trailing_stop max is 20, fits exactly. - H-01 requires
stop_loss→ addstop_lossat 20% as backstop (same threshold as trailing, so trailing fires first in practice) - No filters or risk overlays needed — DCA is intentionally simple.
Spec:
{
"meta": {
"name": "btc_weekly_dca",
"version": "1.0",
"risk_tier": "passive",
"description": "Weekly DCA into BTC with trailing stop safety net",
"author_intent": "DCA $100 into BTC every Monday at 9am UTC. No exit, but add a 20% trailing stop for flash-crash protection."
},
"instrument": {
"symbol": "WBTC-USDC",
"timeframe": "1D"
},
"entry": {
"type": "time_schedule",
"interval": "1W",
"anchor_utc": "09:00"
},
"exit": {
"stop_loss": { "pct": 20 },
"trailing_stop": { "pct": 20 }
},
"sizing": {
"type": "fixed_usd",
"usd": 100
},
"filters": [],
"risk_overlays": []
}Graduation: Backtestable. Deterministic schedule + price-based exits. Run on 24 months of BTC daily bars.
---
Example 3: Meme Safety-First — Full Safety Stack
User prompt:
"I want to snipe new meme coins ranked in the top 20 trending but I don't want to get rugged. Check everything — honeypot, LP burns, taxes under 5%, no bundler pumps, dev holding under 10%, at least one smart money already in. Risk $50 per trade, tiered take-profit at 2x/5x/10x, hard stop at -50%."
Reasoning:
- Top 20 trending →
ranking_entrywithlist_name: "trending",top_n: 20 - Dynamic universe →
symbol: "*", needsuniverseblock - "Check everything" → all 13 safety filters
- Taxes under 5% → separate
buy_tax_max+sell_tax_maxatmax_pct: 5 - "At least one smart money in" →
smart_money_present_minfilter withmin_wallets: 1 - Tiered TP at 2x/5x/10x →
tiered_take_profitwithpct_gainvalues of 100/400/900 (2x = +100%, 5x = +400%, 10x = +900%) - Hard stop -50% →
stop_loss.pct: 20(capped at schema max of 20 — inform user) - Implied:
mcap_range+launch_agefor "new meme coin" - All safety filters + ranking_entry →
live_only: true
Spec:
{
"meta": {
"name": "meme_safety_first",
"version": "1.0",
"risk_tier": "aggressive",
"live_only": true,
"description": "Snipe trending meme tokens with full safety filter stack",
"author_intent": "Snipe new meme coins ranked in the top 20 trending, check everything for safety, $50 per trade, tiered TP at 2x/5x/10x, hard stop at -50%."
},
"instrument": {
"symbol": "*",
"timeframe": "5m"
},
"universe": {
"selector": "ranking_entry",
"chain": "solana"
},
"entry": {
"type": "ranking_entry",
"list_name": "trending",
"top_n": 20
},
"exit": {
"stop_loss": { "pct": 20 },
"tiered_take_profit": {
"tiers": [
{ "pct_gain": 100, "pct_sell": 33 },
{ "pct_gain": 400, "pct_sell": 33 },
{ "pct_gain": 900, "pct_sell": 34 }
]
}
},
"sizing": {
"type": "fixed_usd",
"usd": 50
},
"filters": [
{ "type": "mcap_range", "min_usd": 100000, "max_usd": 5000000 },
{ "type": "launch_age", "min_hours": 2, "max_hours": 168 },
{ "type": "honeypot_check" },
{ "type": "lp_locked", "min_pct_locked": 80, "min_lock_days": 30 },
{ "type": "buy_tax_max", "max_pct": 5 },
{ "type": "sell_tax_max", "max_pct": 5 },
{ "type": "liquidity_min", "min_usd": 25000 },
{ "type": "top_holders_max", "top_n": 10, "max_pct": 35 },
{ "type": "bundler_ratio_max", "max_pct": 10 },
{ "type": "dev_holding_max", "max_pct": 10 },
{ "type": "insider_holding_max", "max_pct": 15 },
{ "type": "fresh_wallet_ratio_max", "max_pct": 25 },
{ "type": "smart_money_present_min", "min_wallets": 1 },
{ "type": "phishing_exclude" },
{ "type": "whale_concentration_max", "max_pct": 20 }
],
"risk_overlays": [
{ "type": "max_concurrent_positions", "n": 5 },
{ "type": "max_daily_trades", "n": 10 },
{ "type": "session_loss_pause", "max_consecutive_losses": 3 }
]
}Note: User asked for -50% stop but stop_loss.pct max is 20. Inform the user: "Schema enforces a maximum 20% stop loss for safety. Your position will be stopped at -20% instead of -50%."
Graduation: live_only. Paper gate: >= 10 paper trades + >= 5 live micro-trades at $5 + >= 7 days observation, then $50 sizing unlocks.
---
Example 4: Smart Money Copy-Trade
User prompt:
"Copy-trade these 3 wallets on Base: 0xabc..., 0xdef..., 0x123.... When any of them buys a token, I buy the same token with 2% of my portfolio. Mirror their sells too. Also bail immediately if the dev dumps, or if liquidity drops under $50k. Pause the whole thing if I'm down more than 15% this week."
Reasoning:
- Named wallets →
wallet_copy_buywithtarget_walletas array - Mirror sells →
wallet_mirror_sellinexit.other[] - "Bail if dev dumps" →
dev_dumpinexit.other[] - "Bail if price crashes" (liquidity proxy) →
fast_dump_exitinexit.other[] - Pre-entry liquidity gate →
liquidity_minfilter - 2% of portfolio →
fixed_pct - Week down 15% →
drawdown_pausewithpause_pct: 15 - Add
honeypot_check+phishing_exclude— copy-trading without these is reckless - Dynamic universe →
symbol: "*"+universeblock
Spec:
{
"meta": {
"name": "smart_money_copy",
"version": "1.0",
"risk_tier": "moderate",
"live_only": true,
"description": "Copy-trade 3 wallets on Base with safety exits",
"author_intent": "Copy-trade 3 wallets on Base, 2% portfolio per trade, mirror sells, bail on dev dump or liquidity drop, pause at 15% weekly drawdown."
},
"instrument": {
"symbol": "*",
"timeframe": "5m"
},
"universe": {
"selector": "wallet_copy_buy",
"chain": "base"
},
"entry": {
"type": "wallet_copy_buy",
"target_wallet": [
"0xabc0000000000000000000000000000000000abc",
"0xdef0000000000000000000000000000000000def",
"0x1230000000000000000000000000000000000123"
],
"min_usd": 100,
"mirror_mode": "instant"
},
"exit": {
"stop_loss": { "pct": 15 },
"other": [
{
"type": "wallet_mirror_sell",
"target_wallet": [
"0xabc0000000000000000000000000000000000abc",
"0xdef0000000000000000000000000000000000def",
"0x1230000000000000000000000000000000000123"
],
"min_pct_sold": 50
},
{ "type": "dev_dump", "min_usd": 500 },
{ "type": "fast_dump_exit", "drop_pct": 30, "window_sec": 60 }
]
},
"sizing": {
"type": "fixed_pct",
"pct": 2
},
"filters": [
{ "type": "liquidity_min", "min_usd": 50000 },
{ "type": "honeypot_check" },
{ "type": "phishing_exclude" }
],
"risk_overlays": [
{ "type": "drawdown_pause", "pause_pct": 15 },
{ "type": "max_concurrent_positions", "n": 8 },
{ "type": "correlation_cap", "mode": "same_token_dedupe", "max_correlated": 3 }
]
}Graduation: live_only. Wallet activity can't be replayed. Paper gate: >= 10 paper + >= 5 live micro + >= 7 days.
---
Example 5: Launchpad Sniper
User prompt:
"Snipe brand-new tokens on the OKX launchpad — only tokens launched in the last 48 hours. Require at least 2 smart-money wallets to be in already, no bundlers over 5%, LP must be locked, mcap between $50k and $2M. $75 per trade, max 3 positions at once. Take profits at 1.5x / 3x / 6x. Bail if price drops more than 40% in 5 minutes."
Reasoning:
- "Brand-new tokens on launchpad" →
ranking_entrywithlist_name: "new",top_n: 50 - "Last 48 hours" →
launch_agefilter withmax_hours: 48 - "2 smart money in" →
smart_money_present_minwithmin_wallets: 2 - "No bundlers over 5%" →
bundler_ratio_maxwithmax_pct: 5 - "LP locked" →
lp_lockedwithmin_pct_locked: 80 - "Mcap $50k-$2M" →
mcap_range - $75/trade →
fixed_usd - Max 3 positions →
max_concurrent_positions - TP at 1.5x/3x/6x →
tiered_take_profitwithpct_gain50/200/500 - "Bail if drops 40% in 5 min" →
fast_dump_exitwithdrop_pct: 40,window_sec: 300 - Add
stop_lossbackstop at 20% so failed snipes don't bleed forever - Add
honeypot_check+phishing_excludeas baseline safety
Spec:
{
"meta": {
"name": "launchpad_sniper",
"version": "1.0",
"risk_tier": "speculative",
"live_only": true,
"description": "Snipe new tokens under 48h with SM confirmation and safety stack",
"author_intent": "Snipe brand-new tokens, last 48 hours, require 2 SM wallets, no bundlers over 5%, LP locked, mcap $50k-$2M, $75/trade, max 3 positions, TP at 1.5x/3x/6x, bail on 40% drop in 5 min."
},
"instrument": {
"symbol": "*",
"timeframe": "1m"
},
"universe": {
"selector": "ranking_entry",
"chain": "solana"
},
"entry": {
"type": "ranking_entry",
"list_name": "new",
"top_n": 50
},
"exit": {
"stop_loss": { "pct": 20 },
"tiered_take_profit": {
"tiers": [
{ "pct_gain": 50, "pct_sell": 40 },
{ "pct_gain": 200, "pct_sell": 30 },
{ "pct_gain": 500, "pct_sell": 30 }
]
},
"other": [
{ "type": "fast_dump_exit", "drop_pct": 40, "window_sec": 300 }
]
},
"sizing": {
"type": "fixed_usd",
"usd": 75
},
"filters": [
{ "type": "launch_age", "max_hours": 48 },
{ "type": "mcap_range", "min_usd": 50000, "max_usd": 2000000 },
{ "type": "lp_locked", "min_pct_locked": 80, "min_lock_days": 30 },
{ "type": "bundler_ratio_max", "max_pct": 5 },
{ "type": "smart_money_present_min", "min_wallets": 2 },
{ "type": "honeypot_check" },
{ "type": "phishing_exclude" }
],
"risk_overlays": [
{ "type": "max_concurrent_positions", "n": 3 },
{ "type": "session_loss_pause", "max_consecutive_losses": 3 }
]
}Graduation: live_only. Paper gate: >= 10 paper + >= 5 live micro-trades at $10 + >= 7 days, then $75 sizing unlocks.
---
10. Example Coverage Scorecard
| Category | Hit | Miss (unhit in examples, but available) |
|---|---|---|
| Entry (12) | price_drop, time_schedule, ranking_entry, wallet_copy_buy | price_breakout, ma_cross, rsi_threshold, volume_spike, smart_money_buy, dev_buy, macd_cross, bollinger_touch |
| Exit (10) | stop_loss, take_profit, trailing_stop, tiered_take_profit, wallet_mirror_sell, dev_dump, fast_dump_exit | time_exit, indicator_reversal, smart_money_sell |
| Filter (23) | time_window, cooldown, mcap_range, launch_age + all 13 safety | volatility_range, volume_minimum, market_regime, price_range, btc_overlay, top_zone_guard |
| Sizing (3) | fixed_usd, fixed_pct | volatility_scaled |
| Risk (5) | max_daily_trades, max_concurrent_positions, drawdown_pause, correlation_cap, session_loss_pause | (all covered) |
The 5 examples cover 33/53 primitives. The remaining 20 are straightforward — refer to the primitive tables above for their exact param names and ranges.
{
"name": "starter-coach",
"description": "Starter Coach V2 — conversational 6-step skill that guides users to build their own automated DEX spot-trading bot on OKX DEX. Onboard → Profile → Build Strategy → Paper Trade → Go Live. Uses OnchainOS CLI for all on-chain data and execution.",
"version": "1.0.0",
"author": {
"name": "VibeCodeDaddy",
"github": "VibeCodeDaddy69"
},
"license": "MIT",
"keywords": [
"trading-bot",
"strategy-builder",
"coaching",
"paper-trade",
"backtesting",
"dex",
"onchainos",
"solana",
"ethereum"
],
"repository": "https://github.com/okx/plugin-store"
}
"""
Backtest engine — replay a validated spec against historical OHLCV data.
Hard-rejects any spec where live_only == true (must go through paper_gate instead).
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from typing import Any
from harness import validate_spec, LIVE_ONLY_TYPES
from primitives.entry import MarketContext, Bar, evaluate_entry
from primitives.exit import Position, ExitSignal, evaluate_all_exits
from primitives.filter import evaluate_filters
from primitives.sizing import compute_size
from primitives.risk import PortfolioState, check_all_overlays
@dataclass
class Trade:
token: str
entry_price: float
exit_price: float
entry_bar: int
exit_bar: int
size_usd: float
pnl_usd: float
pnl_pct: float
exit_reason: str
@dataclass
class BacktestResult:
ok: bool = False
error: str = ""
trades: list[Trade] = field(default_factory=list)
total_pnl_usd: float = 0.0
total_pnl_pct: float = 0.0
win_rate: float = 0.0
max_drawdown_pct: float = 0.0
sharpe: float = 0.0
trade_count: int = 0
def _compute_indicators(bars: list[Bar], spec: dict) -> dict[str, Any]:
"""Pre-compute indicators needed by the spec's entry/exit/filters."""
closes = [b.close for b in bars]
indicators: dict[str, Any] = {"ema": {}, "sma": {}, "rsi": {}, "macd": {}, "bbands": {}}
# Collect periods needed
entry = spec.get("entry", {})
etype = entry.get("type", "")
# SMA/EMA
for period in _extract_ma_periods(spec):
if len(closes) >= period:
indicators["sma"][period] = _sma(closes, period)
indicators["ema"][period] = _ema(closes, period)
# RSI
if etype == "rsi_threshold":
p = entry.get("period", 14)
indicators["rsi"][p] = _rsi(closes, p)
# MACD
if etype == "macd_cross":
fast = entry.get("fast_period", 12)
slow = entry.get("slow_period", 26)
sig = entry.get("signal_period", 9)
macd_l, signal_l, hist_l = _macd(closes, fast, slow, sig)
indicators["macd"] = {"macd": macd_l, "signal": signal_l, "hist": hist_l}
# Bollinger Bands
if etype == "bollinger_touch":
p = entry.get("period", 20)
std = entry.get("std_dev", 2.0)
upper, middle, lower = _bbands(closes, p, std)
indicators["bbands"] = {"upper": upper, "middle": middle, "lower": lower}
return indicators
def _extract_ma_periods(spec: dict) -> set[int]:
periods: set[int] = set()
entry = spec.get("entry", {})
if entry.get("type") == "ma_cross":
periods.add(entry.get("fast_period", 10))
periods.add(entry.get("slow_period", 50))
for filt in spec.get("filters", []):
if filt.get("type") == "market_regime":
periods.add(filt.get("ma_period", 200))
return periods
def _sma(values: list[float], period: int) -> list[float]:
result: list[float] = []
for i in range(len(values)):
if i < period - 1:
result.append(0.0)
else:
result.append(sum(values[i - period + 1:i + 1]) / period)
return result
def _ema(values: list[float], period: int) -> list[float]:
result: list[float] = []
k = 2 / (period + 1)
for i, v in enumerate(values):
if i == 0:
result.append(v)
else:
result.append(v * k + result[-1] * (1 - k))
return result
def _rsi(values: list[float], period: int) -> list[float]:
result: list[float] = [50.0] # default for first value
gains: list[float] = []
losses: list[float] = []
for i in range(1, len(values)):
delta = values[i] - values[i - 1]
gains.append(max(delta, 0))
losses.append(max(-delta, 0))
if i < period:
result.append(50.0)
continue
if i == period:
avg_gain = sum(gains[-period:]) / period
avg_loss = sum(losses[-period:]) / period
else:
avg_gain = (avg_gain * (period - 1) + gains[-1]) / period
avg_loss = (avg_loss * (period - 1) + losses[-1]) / period
if avg_loss == 0:
result.append(100.0)
else:
rs = avg_gain / avg_loss
result.append(100 - 100 / (1 + rs))
return result
def _macd(values: list[float], fast: int, slow: int, signal: int):
fast_ema = _ema(values, fast)
slow_ema = _ema(values, slow)
macd_line = [f - s for f, s in zip(fast_ema, slow_ema)]
signal_line = _ema(macd_line, signal)
hist = [m - s for m, s in zip(macd_line, signal_line)]
return macd_line, signal_line, hist
def _bbands(values: list[float], period: int, std_dev: float):
sma_vals = _sma(values, period)
upper, lower = [], []
for i in range(len(values)):
if i < period - 1:
upper.append(0.0)
lower.append(0.0)
else:
window = values[i - period + 1:i + 1]
mean = sma_vals[i]
variance = sum((x - mean) ** 2 for x in window) / period
std = variance ** 0.5
upper.append(mean + std_dev * std)
lower.append(mean - std_dev * std)
return upper, sma_vals, lower
def run_backtest(
spec: dict,
bars: list[dict],
initial_equity: float = 10000.0,
) -> BacktestResult:
"""
Run a backtest on historical data.
Args:
spec: validated strategy spec dict
bars: list of {ts, open, high, low, close, volume} dicts
initial_equity: starting capital in USD
Returns:
BacktestResult
"""
# 1. Validate
ok, errors, meta = validate_spec(spec)
if not ok:
return BacktestResult(ok=False, error=f"Spec validation failed: {'; '.join(errors)}")
if meta["live_only"]:
return BacktestResult(
ok=False,
error="live_only spec cannot be backtested. Use paper_gate for graduation."
)
# 2. Parse bars
parsed_bars = [
Bar(ts=b["ts"], open=b["open"], high=b["high"],
low=b["low"], close=b["close"], volume=b["volume"])
for b in bars
]
if len(parsed_bars) < 50:
return BacktestResult(ok=False, error="Insufficient bars (need >= 50)")
# 3. Pre-compute indicators
indicators = _compute_indicators(parsed_bars, spec)
# 4. Simulate
equity = initial_equity
peak_equity = equity
max_dd = 0.0
trades: list[Trade] = []
position: Position | None = None
portfolio = PortfolioState(
equity_usd=equity, peak_equity_usd=equity, session_start_ts=parsed_bars[0].ts
)
returns: list[float] = []
entry_spec = spec["entry"]
exit_spec = spec["exit"]
sizing_spec = spec["sizing"]
filters = spec.get("filters", [])
overlays = spec.get("risk_overlays", [])
for i in range(50, len(parsed_bars)):
ctx = MarketContext(
bars=parsed_bars[:i + 1],
current_price=parsed_bars[i].close,
timeframe=spec.get("instrument", {}).get("timeframe", "5m"),
token=spec.get("instrument", {}).get("symbol", ""),
)
ctx.ema = indicators.get("ema", {})
ctx.sma = indicators.get("sma", {})
ctx.rsi = indicators.get("rsi", {})
ctx.macd = indicators.get("macd", {})
ctx.bbands = indicators.get("bbands", {})
# Check exits first
if position is not None:
sig = evaluate_all_exits(ctx, position, exit_spec)
if sig:
pnl_pct = (ctx.current_price - position.entry_price) / position.entry_price * 100
sell_frac = sig.sell_pct / 100
pnl_usd = position.size_usd * sell_frac * pnl_pct / 100
trades.append(Trade(
token=position.token, entry_price=position.entry_price,
exit_price=ctx.current_price, entry_bar=position.entry_bar_idx,
exit_bar=i, size_usd=position.size_usd * sell_frac,
pnl_usd=pnl_usd, pnl_pct=pnl_pct, exit_reason=sig.reason,
))
equity += pnl_usd
returns.append(pnl_pct)
if pnl_usd < 0:
portfolio.consecutive_losses += 1
else:
portfolio.consecutive_losses = 0
if sig.sell_pct >= 100:
position = None
portfolio.open_positions -= 1
else:
position.size_usd *= (1 - sell_frac)
portfolio.equity_usd = equity
if equity > peak_equity:
peak_equity = equity
portfolio.peak_equity_usd = peak_equity
dd = (peak_equity - equity) / peak_equity * 100 if peak_equity > 0 else 0
max_dd = max(max_dd, dd)
# Try entry if no position
if position is None:
# Check risk overlays
portfolio.equity_usd = equity
overlay_ok, _ = check_all_overlays(portfolio, overlays)
if not overlay_ok:
continue
# Check filters
filter_ok, _ = evaluate_filters(ctx, filters)
if not filter_ok:
continue
# Check entry
if evaluate_entry(ctx, entry_spec):
size = compute_size(equity, ctx, sizing_spec)
if size > 0:
position = Position(
token=ctx.token, entry_price=ctx.current_price,
entry_ts=parsed_bars[i].ts, entry_bar_idx=i,
size_usd=size, peak_price=ctx.current_price,
)
portfolio.open_positions += 1
portfolio.trades_today += 1
# 5. Close any remaining position at last bar
if position is not None:
final_price = parsed_bars[-1].close
pnl_pct = (final_price - position.entry_price) / position.entry_price * 100
pnl_usd = position.size_usd * pnl_pct / 100
trades.append(Trade(
token=position.token, entry_price=position.entry_price,
exit_price=final_price, entry_bar=position.entry_bar_idx,
exit_bar=len(parsed_bars) - 1, size_usd=position.size_usd,
pnl_usd=pnl_usd, pnl_pct=pnl_pct, exit_reason="end_of_data",
))
equity += pnl_usd
returns.append(pnl_pct)
# 6. Compute stats
total_pnl = equity - initial_equity
total_pnl_pct = total_pnl / initial_equity * 100 if initial_equity > 0 else 0
wins = sum(1 for t in trades if t.pnl_usd > 0)
win_rate = wins / len(trades) * 100 if trades else 0
sharpe = 0.0
if returns:
avg_ret = sum(returns) / len(returns)
if len(returns) > 1:
variance = sum((r - avg_ret) ** 2 for r in returns) / (len(returns) - 1)
std_ret = variance ** 0.5
sharpe = (avg_ret / std_ret) * (252 ** 0.5) if std_ret > 0 else 0
return BacktestResult(
ok=True,
trades=trades,
total_pnl_usd=round(total_pnl, 2),
total_pnl_pct=round(total_pnl_pct, 2),
win_rate=round(win_rate, 1),
max_drawdown_pct=round(max_dd, 2),
sharpe=round(sharpe, 2),
trade_count=len(trades),
)
"""
Harness — validate strategy specs against schema.json + forbidden-pattern checks.
Usage:
from harness import validate_spec
ok, errors = validate_spec(spec_dict)
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
try:
import jsonschema
from jsonschema import Draft202012Validator
except ImportError:
jsonschema = None # type: ignore
Draft202012Validator = None # type: ignore
SCHEMA_PATH = Path(__file__).parent / "schema.json"
# Primitives with x-live-only: true in schema.json
LIVE_ONLY_TYPES: set[str] = {
# entries
"smart_money_buy", "dev_buy", "ranking_entry", "wallet_copy_buy",
# exits
"smart_money_sell", "dev_dump", "wallet_mirror_sell", "fast_dump_exit",
# filters
"mcap_range", "launch_age",
"honeypot_check", "lp_locked", "buy_tax_max", "sell_tax_max",
"liquidity_min", "top_holders_max", "bundler_ratio_max",
"dev_holding_max", "insider_holding_max", "fresh_wallet_ratio_max",
"smart_money_present_min", "phishing_exclude", "whale_concentration_max",
}
def _load_schema() -> dict:
with open(SCHEMA_PATH) as f:
return json.load(f)
def _collect_types(spec: dict) -> set[str]:
"""Walk the spec and collect every primitive type used."""
types: set[str] = set()
entry = spec.get("entry", {})
if isinstance(entry, dict) and "type" in entry:
types.add(entry["type"])
exit_block = spec.get("exit", {})
for inner_key in ("stop_loss", "take_profit", "trailing_stop", "tiered_take_profit"):
if inner_key in exit_block:
types.add(inner_key)
for other in exit_block.get("other", []):
if isinstance(other, dict) and "type" in other:
types.add(other["type"])
for filt in spec.get("filters", []):
if isinstance(filt, dict) and "type" in filt:
types.add(filt["type"])
sizing = spec.get("sizing", {})
if isinstance(sizing, dict) and "type" in sizing:
types.add(sizing["type"])
for overlay in spec.get("risk_overlays", []):
if isinstance(overlay, dict) and "type" in overlay:
types.add(overlay["type"])
return types
def _check_live_only(spec: dict) -> bool:
"""Return True if any primitive in the spec is live_only."""
return bool(_collect_types(spec) & LIVE_ONLY_TYPES)
def _check_h01(spec: dict, errors: list[str]) -> None:
"""H-01: stop_loss is required."""
exit_block = spec.get("exit", {})
if "stop_loss" not in exit_block:
errors.append("H-01: exit.stop_loss is required. A strategy without a stop is gambling.")
elif not isinstance(exit_block["stop_loss"], dict) or "pct" not in exit_block["stop_loss"]:
errors.append("H-01: exit.stop_loss must have a 'pct' field.")
def _check_h02(spec: dict, errors: list[str]) -> None:
"""H-02: No martingale / averaging down."""
meta = spec.get("meta", {})
for field in ("description", "author_intent", "name"):
text = str(meta.get(field, "")).lower()
for keyword in ("martingale", "double down", "average down", "averaging down"):
if keyword in text:
errors.append(f"H-02: Martingale / averaging down detected in meta.{field}. Rejected.")
return
def _check_h03(spec: dict, errors: list[str]) -> None:
"""H-03: stop_loss.pct must be < take_profit.pct (when take_profit is used)."""
exit_block = spec.get("exit", {})
sl = exit_block.get("stop_loss", {})
tp = exit_block.get("take_profit", {})
if not sl or not tp:
return
sl_pct = sl.get("pct", 0)
tp_pct = tp.get("pct", 0)
if sl_pct and tp_pct and sl_pct >= tp_pct:
errors.append(
f"H-03: stop_loss ({sl_pct}%) must be tighter than take_profit ({tp_pct}%). "
"Negative asymmetry rejected."
)
def _check_h05(spec: dict, errors: list[str]) -> None:
"""H-05: sizing.pct * max_daily_trades.n <= 20% daily risk cap."""
sizing = spec.get("sizing", {})
if sizing.get("type") != "fixed_pct":
return
pct = sizing.get("pct", 0)
for overlay in spec.get("risk_overlays", []):
if overlay.get("type") == "max_daily_trades":
n = overlay.get("n", 1)
daily_risk = pct * n
if daily_risk > 20:
errors.append(
f"H-05: fixed_pct ({pct}%) x max_daily_trades ({n}) = {daily_risk}% "
"exceeds 20% daily risk cap."
)
return
def _check_h06(spec: dict, errors: list[str]) -> None:
"""H-06: No unknown primitive types."""
known_types = {
"price_drop", "price_breakout", "ma_cross", "rsi_threshold",
"volume_spike", "time_schedule", "smart_money_buy", "dev_buy",
"macd_cross", "bollinger_touch", "ranking_entry", "wallet_copy_buy",
"time_exit", "indicator_reversal", "smart_money_sell", "dev_dump",
"wallet_mirror_sell", "fast_dump_exit",
"time_window", "volatility_range", "volume_minimum", "cooldown",
"market_regime", "price_range", "btc_overlay", "top_zone_guard",
"mcap_range", "launch_age",
"honeypot_check", "lp_locked", "buy_tax_max", "sell_tax_max",
"liquidity_min", "top_holders_max", "bundler_ratio_max",
"dev_holding_max", "insider_holding_max", "fresh_wallet_ratio_max",
"smart_money_present_min", "phishing_exclude", "whale_concentration_max",
"fixed_pct", "fixed_usd", "volatility_scaled",
"max_daily_trades", "max_concurrent_positions", "drawdown_pause",
"correlation_cap", "session_loss_pause",
}
for t in _collect_types(spec):
if t in ("stop_loss", "take_profit", "trailing_stop", "tiered_take_profit"):
continue
if t not in known_types:
errors.append(f"H-06: Unknown primitive type '{t}'. No freeform code allowed.")
def validate_spec(spec: dict[str, Any]) -> tuple[bool, list[str], dict[str, Any]]:
"""
Validate a strategy spec.
Returns:
(ok, errors, meta)
- ok: True if spec is valid
- errors: list of human-readable error strings
- meta: {"live_only": bool, "primitive_count": int, "types_used": list[str]}
"""
errors: list[str] = []
# 1. JSON Schema validation (H-04 param bounds + structural)
if Draft202012Validator is not None:
schema = _load_schema()
validator = Draft202012Validator(schema)
for error in sorted(validator.iter_errors(spec), key=lambda e: list(e.path)):
path = ".".join(str(p) for p in error.absolute_path) or "(root)"
errors.append(f"Schema: {path} — {error.message}")
else:
errors.append("Warning: jsonschema not installed. Run: pip install jsonschema")
# 2. Forbidden-pattern checks
_check_h01(spec, errors)
_check_h02(spec, errors)
_check_h03(spec, errors)
_check_h05(spec, errors)
_check_h06(spec, errors)
# 3. Derive metadata
types_used = sorted(_collect_types(spec))
live_only = _check_live_only(spec)
meta = {
"live_only": live_only,
"primitive_count": len(types_used),
"types_used": types_used,
}
return len(errors) == 0, errors, meta
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python harness.py <spec.json>")
sys.exit(1)
with open(sys.argv[1]) as f:
spec = json.load(f)
ok, errors, meta = validate_spec(spec)
if ok:
print(f"PASS live_only={meta['live_only']} primitives={meta['primitive_count']}")
print(f" types: {', '.join(meta['types_used'])}")
else:
print(f"FAIL ({len(errors)} errors)")
for e in errors:
print(f" - {e}")
sys.exit(1)
MIT License
Copyright (c) 2026 VibeCodeDaddy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
Live execution engine — real-time position monitoring + trade execution.
Ties together:
- onchainos.py (data + swap execution + wallet monitoring)
- primitives/* (entry/exit/filter/sizing/risk evaluators)
- paper_gate.py (graduation-gated sizing)
- harness.py (spec validation)
Usage:
from live_engine import LiveEngine
engine = LiveEngine(spec, wallet_address="...")
engine.start() # blocking loop
"""
from __future__ import annotations
import json
import time
import logging
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Any
from harness import validate_spec
from onchainos import OnchainOS, SwapResult
from primitives.entry import MarketContext, Bar, evaluate_entry
from primitives.exit import Position, ExitSignal, evaluate_all_exits
from primitives.filter import evaluate_filters
from primitives.sizing import compute_size
from primitives.risk import PortfolioState, check_all_overlays
from paper_gate import get_allowed_size_multiplier, record_paper_trade
log = logging.getLogger("live_engine")
# ── Config ────────────────────────────────────────────────────────
TICK_INTERVAL_SEC = 30 # Poll interval for price/candle checks
BALANCE_POLL_SEC = 60 # Poll interval for wallet balance sync
POSITION_STATE_DIR = Path(__file__).parent / ".live_state"
# Common stablecoin addresses per chain (quote tokens for swaps)
QUOTE_TOKENS: dict[str, str] = {
"solana": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", # USDC on Solana
"ethereum": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # USDC on Ethereum
"base": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC on Base
"bsc": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", # USDC on BSC
"arbitrum": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC on Arbitrum
}
# ── Position Tracking ─────────────────────────────────────────────
@dataclass
class LivePosition:
"""A tracked open position."""
token_address: str = ""
token_symbol: str = ""
entry_price: float = 0.0
entry_ts: float = 0.0
entry_tx_hash: str = ""
size_usd: float = 0.0
token_amount: float = 0.0 # actual tokens held
peak_price: float = 0.0 # for trailing stop
current_price: float = 0.0
unrealized_pnl_usd: float = 0.0
unrealized_pnl_pct: float = 0.0
bars_held: int = 0
status: str = "open" # open | closing | closed
@dataclass
class EngineState:
"""Persisted engine state across restarts."""
strategy_name: str = ""
wallet_address: str = ""
chain: str = "solana"
positions: list[dict[str, Any]] = field(default_factory=list)
closed_trades: list[dict[str, Any]] = field(default_factory=list)
equity_usd: float = 0.0
peak_equity_usd: float = 0.0
trades_today: int = 0
consecutive_losses: int = 0
last_trade_day: str = ""
total_trades: int = 0
total_pnl_usd: float = 0.0
started_ts: float = 0.0
last_tick_ts: float = 0.0
def _state_path(strategy_name: str) -> Path:
POSITION_STATE_DIR.mkdir(parents=True, exist_ok=True)
return POSITION_STATE_DIR / f"{strategy_name}.json"
def _load_engine_state(strategy_name: str) -> EngineState:
path = _state_path(strategy_name)
if not path.exists():
return EngineState(strategy_name=strategy_name, started_ts=time.time())
with open(path) as f:
data = json.load(f)
return EngineState(**data)
def _save_engine_state(state: EngineState) -> None:
state.last_tick_ts = time.time()
path = _state_path(state.strategy_name)
with open(path, "w") as f:
json.dump(asdict(state), f, indent=2)
# ── Live Engine ───────────────────────────────────────────────────
class LiveEngine:
"""
Real-time execution engine for a validated strategy spec.
Lifecycle:
1. validate spec
2. resolve wallet address
3. sync initial balances
4. enter tick loop:
a. fetch latest candles + price
b. update position prices + P&L
c. evaluate exits → fire sells
d. evaluate entries → fire buys
e. persist state
"""
def __init__(
self,
spec: dict[str, Any],
wallet_address: str | None = None,
initial_equity: float = 0.0,
paper_mode: bool = False,
):
# Validate spec
ok, errors, meta = validate_spec(spec)
if not ok:
raise ValueError(f"Invalid spec: {'; '.join(errors)}")
self.spec = spec
self.meta = meta
self.paper_mode = paper_mode
# Extract spec components
self.strategy_name = spec["meta"]["name"]
self.chain = spec.get("universe", {}).get("chain", "solana")
self.symbol = spec["instrument"]["symbol"]
self.timeframe = spec["instrument"]["timeframe"]
self.entry_spec = spec["entry"]
self.exit_spec = spec["exit"]
self.sizing_spec = spec["sizing"]
self.filters = spec.get("filters", [])
self.overlays = spec.get("risk_overlays", [])
# OnchainOS client
self.os = OnchainOS(chain=self.chain)
# Resolve wallet
self.wallet = wallet_address or self.os.get_wallet_address() or ""
if not self.wallet and not paper_mode:
raise ValueError("No wallet address. Log in via `onchainos wallet login`.")
# Quote token for swaps
self.quote_token = QUOTE_TOKENS.get(self.chain, "")
# Graduation multiplier (0.0 = paper, 0.1 = micro, 1.0 = full)
self.size_multiplier = get_allowed_size_multiplier(self.strategy_name)
# Load or create engine state
self.state = _load_engine_state(self.strategy_name)
self.state.wallet_address = self.wallet
self.state.chain = self.chain
if initial_equity > 0:
self.state.equity_usd = initial_equity
self.state.peak_equity_usd = initial_equity
# Rebuild open positions from state
self.positions: list[LivePosition] = []
for p_data in self.state.positions:
self.positions.append(LivePosition(**p_data))
# Portfolio state for risk overlays
self.portfolio = PortfolioState(
trades_today=self.state.trades_today,
open_positions=len(self.positions),
open_tokens=[p.token_symbol for p in self.positions],
equity_usd=self.state.equity_usd,
peak_equity_usd=self.state.peak_equity_usd,
consecutive_losses=self.state.consecutive_losses,
session_start_ts=self.state.started_ts,
)
log.info(
f"LiveEngine initialized: {self.strategy_name} "
f"chain={self.chain} wallet={self.wallet[:8]}... "
f"positions={len(self.positions)} equity=${self.state.equity_usd:.2f} "
f"multiplier={self.size_multiplier}"
)
# ── Main Loop ─────────────────────────────────────────────────
def start(self) -> None:
"""Blocking tick loop. Ctrl-C to stop."""
log.info(f"Starting live engine for {self.strategy_name}")
self._sync_balances()
try:
while True:
self._tick()
time.sleep(TICK_INTERVAL_SEC)
except KeyboardInterrupt:
log.info("Engine stopped by user")
finally:
self._persist()
def run_once(self) -> dict[str, Any]:
"""Run a single tick (for testing or cron-based execution)."""
return self._tick()
# ── Tick ──────────────────────────────────────────────────────
def _tick(self) -> dict[str, Any]:
"""Single evaluation cycle."""
tick_result: dict[str, Any] = {
"ts": time.time(),
"exits": [],
"entries": [],
"errors": [],
}
# Reset daily counters if new day
today = time.strftime("%Y-%m-%d")
if self.state.last_trade_day != today:
self.state.trades_today = 0
self.state.last_trade_day = today
self.portfolio.trades_today = 0
try:
# Resolve target token(s)
tokens = self._resolve_tokens()
if not tokens:
return tick_result
for token_addr, token_symbol in tokens:
# Build market context
ctx = self._build_context(token_addr, token_symbol)
if ctx is None:
continue
# 1. Check exits for open positions on this token
for pos in self.positions:
if pos.token_address != token_addr or pos.status != "open":
continue
pos.current_price = ctx.current_price
pos.unrealized_pnl_pct = (
(ctx.current_price - pos.entry_price) / pos.entry_price * 100
if pos.entry_price > 0 else 0
)
pos.unrealized_pnl_usd = pos.size_usd * pos.unrealized_pnl_pct / 100
if ctx.current_price > pos.peak_price:
pos.peak_price = ctx.current_price
pos.bars_held += 1
position_obj = Position(
token=token_symbol,
entry_price=pos.entry_price,
entry_ts=pos.entry_ts,
entry_bar_idx=0,
size_usd=pos.size_usd,
peak_price=pos.peak_price,
)
sig = evaluate_all_exits(ctx, position_obj, self.exit_spec)
if sig:
exit_result = self._execute_exit(pos, sig, ctx)
tick_result["exits"].append(exit_result)
# 2. Check entries if no position on this token
has_position = any(
p.token_address == token_addr and p.status == "open"
for p in self.positions
)
if not has_position:
entry_result = self._try_entry(ctx, token_addr, token_symbol)
if entry_result:
tick_result["entries"].append(entry_result)
except Exception as e:
log.error(f"Tick error: {e}")
tick_result["errors"].append(str(e))
self._persist()
return tick_result
# ── Token Resolution ──────────────────────────────────────────
def _resolve_tokens(self) -> list[tuple[str, str]]:
"""
Resolve which tokens to evaluate this tick.
Fixed symbol: [(token_address, symbol)]
Dynamic universe (*): fetch from rankings/wallets.
"""
if self.symbol != "*":
# Fixed token — symbol is like "SOL-USDC", extract base
base = self.symbol.split("-")[0]
# We need the token address — get from price-info or basic-info
# For now, use the symbol as-is; the engine caller should provide addresses
return [(self.symbol, base)]
# Dynamic universe — fetch from rankings or wallet tracker
entry_type = self.entry_spec.get("type", "")
if entry_type == "ranking_entry":
list_name = self.entry_spec.get("list_name", "trending")
top_n = self.entry_spec.get("top_n", 20)
items = self.os.subscribe_ranking(list_name, top_n)
return [(item.token, item.symbol) for item in items if item.token]
elif entry_type == "wallet_copy_buy":
wallets = self.entry_spec.get("target_wallet", [])
if isinstance(wallets, str):
wallets = [wallets]
events = self.os.track_wallets(wallets, trade_type=1) # buys only
return list({(e.token, e.token[:8]) for e in events if e.token})
elif entry_type == "smart_money_buy":
events = self.os.track_smart_money(trade_type=1)
return list({(e.token, e.token[:8]) for e in events if e.token})
return []
# ── Market Context ────────────────────────────────────────────
def _build_context(self, token_addr: str, token_symbol: str) -> MarketContext | None:
"""Fetch candles + price and build a MarketContext."""
candles = self.os.get_candles(token_addr, bar=self.timeframe, limit=100)
if len(candles) < 10:
return None
bars = [
Bar(
ts=c["ts"], open=c["open"], high=c["high"],
low=c["low"], close=c["close"], volume=c["volume"],
)
for c in candles
]
current_price = bars[-1].close if bars else 0
# If current price is 0, try price-info
if current_price == 0:
price_data = self.os.get_price_info(token_addr)
if "error" not in price_data:
current_price = float(
price_data.get("price", price_data.get("lastPrice", 0)) or 0
)
if current_price == 0:
return None
ctx = MarketContext(
bars=bars,
current_price=current_price,
timeframe=self.timeframe,
token=token_symbol,
)
# Attach on-chain safety data for live_only filters
# ctx.onchainos is read by primitives/filter.py for safety tag checks
if self.meta.get("live_only"):
ctx.onchainos = self.os
ctx.onchainos_tags = self.os.get_safety_tags(token_addr)
return ctx
# ── Entry Execution ───────────────────────────────────────────
def _try_entry(
self, ctx: MarketContext, token_addr: str, token_symbol: str
) -> dict[str, Any] | None:
"""Evaluate entry conditions and execute buy if triggered."""
# Check risk overlays
self.portfolio.equity_usd = self.state.equity_usd
overlay_ok, blockers = check_all_overlays(self.portfolio, self.overlays)
if not overlay_ok:
return None
# Check filters
filter_ok, failing = evaluate_filters(ctx, self.filters)
if not filter_ok:
return None
# Check entry trigger
if not evaluate_entry(ctx, self.entry_spec):
return None
# Compute size (with graduation multiplier)
raw_size = compute_size(self.state.equity_usd, ctx, self.sizing_spec)
size_usd = raw_size * self.size_multiplier
if size_usd <= 0:
return None
log.info(
f"ENTRY signal: {token_symbol} @ ${ctx.current_price:.6f} "
f"size=${size_usd:.2f} (x{self.size_multiplier})"
)
# Execute swap
if self.paper_mode:
tx_hash = f"paper_{int(time.time())}"
token_amount = size_usd / ctx.current_price if ctx.current_price > 0 else 0
else:
result = self.os.swap_execute(
from_token=self.quote_token,
to_token=token_addr,
readable_amount=str(round(size_usd, 2)),
wallet=self.wallet,
)
if not result.ok:
log.error(f"Swap failed: {result.error}")
return {"action": "entry_failed", "error": result.error}
tx_hash = result.tx_hash
token_amount = size_usd / ctx.current_price if ctx.current_price > 0 else 0
# Track position
pos = LivePosition(
token_address=token_addr,
token_symbol=token_symbol,
entry_price=ctx.current_price,
entry_ts=time.time(),
entry_tx_hash=tx_hash,
size_usd=size_usd,
token_amount=token_amount,
peak_price=ctx.current_price,
current_price=ctx.current_price,
)
self.positions.append(pos)
self.portfolio.open_positions += 1
self.portfolio.open_tokens.append(token_symbol)
self.state.trades_today += 1
self.portfolio.trades_today += 1
self.state.total_trades += 1
# Record for paper gate
if self.paper_mode:
record_paper_trade(self.strategy_name)
return {
"action": "entry",
"token": token_symbol,
"price": ctx.current_price,
"size_usd": size_usd,
"tx_hash": tx_hash,
"paper": self.paper_mode,
}
# ── Exit Execution ────────────────────────────────────────────
def _execute_exit(
self, pos: LivePosition, sig: ExitSignal, ctx: MarketContext
) -> dict[str, Any]:
"""Execute a sell based on exit signal."""
sell_frac = sig.sell_pct / 100
sell_usd = pos.size_usd * sell_frac
pnl_pct = (ctx.current_price - pos.entry_price) / pos.entry_price * 100
pnl_usd = sell_usd * pnl_pct / 100
log.info(
f"EXIT signal: {pos.token_symbol} reason={sig.reason} "
f"sell={sig.sell_pct}% pnl={pnl_pct:+.1f}% (${pnl_usd:+.2f})"
)
# Execute sell swap
tx_hash = ""
if self.paper_mode:
tx_hash = f"paper_exit_{int(time.time())}"
else:
# Sell the token amount proportional to sell_pct
sell_amount = pos.token_amount * sell_frac
if sell_amount > 0:
result = self.os.swap_execute(
from_token=pos.token_address,
to_token=self.quote_token,
readable_amount=str(sell_amount),
wallet=self.wallet,
)
if not result.ok:
log.error(f"Exit swap failed: {result.error}")
return {"action": "exit_failed", "error": result.error}
tx_hash = result.tx_hash
# Update state
self.state.equity_usd += pnl_usd
self.state.total_pnl_usd += pnl_usd
if self.state.equity_usd > self.state.peak_equity_usd:
self.state.peak_equity_usd = self.state.equity_usd
self.portfolio.equity_usd = self.state.equity_usd
self.portfolio.peak_equity_usd = self.state.peak_equity_usd
if pnl_usd < 0:
self.state.consecutive_losses += 1
self.portfolio.consecutive_losses += 1
else:
self.state.consecutive_losses = 0
self.portfolio.consecutive_losses = 0
trade_record = {
"token": pos.token_symbol,
"token_address": pos.token_address,
"entry_price": pos.entry_price,
"exit_price": ctx.current_price,
"size_usd": sell_usd,
"pnl_usd": round(pnl_usd, 4),
"pnl_pct": round(pnl_pct, 2),
"reason": sig.reason,
"entry_ts": pos.entry_ts,
"exit_ts": time.time(),
"tx_hash": tx_hash,
"paper": self.paper_mode,
}
if sig.sell_pct >= 100:
pos.status = "closed"
self.portfolio.open_positions -= 1
if pos.token_symbol in self.portfolio.open_tokens:
self.portfolio.open_tokens.remove(pos.token_symbol)
self.state.closed_trades.append(trade_record)
else:
pos.size_usd *= (1 - sell_frac)
pos.token_amount *= (1 - sell_frac)
return {"action": "exit", **trade_record}
# ── Balance Sync ──────────────────────────────────────────────
def _sync_balances(self) -> None:
"""Sync wallet balances with on-chain state."""
if self.paper_mode:
return
balances = self.os.get_all_balances(force=True)
if not balances:
log.warning("Could not fetch wallet balances")
return
# Calculate total equity from balances
total_usd = 0.0
for bal in balances:
if isinstance(bal, dict):
usd_val = float(bal.get("balanceUsd", bal.get("usd_value", 0)) or 0)
total_usd += usd_val
if total_usd > 0:
self.state.equity_usd = total_usd
if total_usd > self.state.peak_equity_usd:
self.state.peak_equity_usd = total_usd
log.info(f"Wallet equity synced: ${total_usd:.2f}")
# ── Position Queries ──────────────────────────────────────────
def get_open_positions(self) -> list[dict[str, Any]]:
"""Return all open positions with current P&L."""
result = []
for pos in self.positions:
if pos.status != "open":
continue
result.append({
"token": pos.token_symbol,
"token_address": pos.token_address,
"entry_price": pos.entry_price,
"current_price": pos.current_price,
"size_usd": pos.size_usd,
"unrealized_pnl_usd": pos.unrealized_pnl_usd,
"unrealized_pnl_pct": pos.unrealized_pnl_pct,
"bars_held": pos.bars_held,
"entry_ts": pos.entry_ts,
})
return result
def get_portfolio_summary(self) -> dict[str, Any]:
"""Return portfolio summary for display."""
open_pos = self.get_open_positions()
total_unrealized = sum(p["unrealized_pnl_usd"] for p in open_pos)
return {
"strategy": self.strategy_name,
"chain": self.chain,
"wallet": self.wallet,
"equity_usd": round(self.state.equity_usd, 2),
"peak_equity_usd": round(self.state.peak_equity_usd, 2),
"total_pnl_usd": round(self.state.total_pnl_usd, 2),
"unrealized_pnl_usd": round(total_unrealized, 2),
"open_positions": len(open_pos),
"total_trades": self.state.total_trades,
"trades_today": self.state.trades_today,
"consecutive_losses": self.state.consecutive_losses,
"size_multiplier": self.size_multiplier,
"paper_mode": self.paper_mode,
"positions": open_pos,
}
def get_trade_history(self) -> list[dict[str, Any]]:
"""Return closed trade history."""
return list(self.state.closed_trades)
# ── Persistence ───────────────────────────────────────────────
def _persist(self) -> None:
"""Save engine state to disk."""
# Serialize open positions
self.state.positions = [
asdict(p) for p in self.positions if p.status == "open"
]
_save_engine_state(self.state)
"""
LLM-driven strategy generation.
Takes a user profile + full primitive catalog → unique spec + theme + tagline.
Falls back gracefully if LLM is unavailable.
"""
from __future__ import annotations
import json
import os
import re
from typing import Any
# ── Primitive catalog (concise form for LLM prompt) ──────────────────────────
PRIMITIVE_CATALOG = """
ENTRY PRIMITIVES (pick exactly one):
- price_drop: {pct, lookback_bars} — buy when price drops X% in window
- price_breakout: {direction, lookback_bars, confirm_pct} — buy on breakout
- ma_cross: {fast_period, slow_period, ma_type} — buy when fast MA crosses slow MA
- rsi_threshold: {period, level, direction} — buy when RSI crosses level (cross_up at 30 = oversold)
- volume_spike: {multiplier, avg_bars} — buy on unusual volume surge
- time_schedule: {interval, anchor_utc} — DCA on fixed schedule (interval: 1D/1W/1M)
- smart_money_buy: {min_wallets, window_min} — buy when N smart wallets buy same token within window
- dev_buy: {min_usd, window_min} — buy when developer wallet buys their own token
- macd_cross: {fast_period, slow_period, signal_period, direction} — buy on MACD crossover
- bollinger_touch: {period, std_dev, band} — buy when price touches band (band: lower/upper)
- ranking_entry: {list_name, top_n} — snipe from list (list_name: trending/gainers/new)
- wallet_copy_buy: {target_wallet[], min_usd, mirror_mode} — mirror wallet buys (mirror_mode: instant/mcap_target)
EXIT PRIMITIVES:
Required field: stop_loss: {pct}
Optional extras in "other" array:
- {type: take_profit, pct} — fixed take profit %
- {type: trailing_stop, pct} — trailing stop %
- {type: tiered_take_profit, tiers:[{pct_gain, pct_sell},...]} — sell in layers (pct_sell values must sum to 100)
- {type: time_exit, max_bars} — exit after N bars
- {type: indicator_reversal, mirror_entry: true} — exit when entry signal reverses
- {type: smart_money_sell, min_wallets, window_min} — exit when smart money sells
- {type: dev_dump, min_usd} — exit on developer dump
- {type: wallet_mirror_sell, target_wallet[], min_pct_sold} — mirror wallet sells
- {type: fast_dump_exit, drop_pct, window_sec} — emergency rug/dump exit
FILTER PRIMITIVES (optional array):
General filters: time_window, volatility_range, volume_minimum, cooldown, market_regime, price_range, btc_overlay, top_zone_guard
Token safety (use for meme/new tokens): mcap_range, launch_age, honeypot_check, lp_locked, buy_tax_max, sell_tax_max, liquidity_min, top_holders_max, bundler_ratio_max, dev_holding_max, insider_holding_max, fresh_wallet_ratio_max, smart_money_present_min, phishing_exclude, whale_concentration_max
SIZING PRIMITIVES (pick one):
- {type: fixed_usd, usd} — fixed dollar amount per trade
- {type: fixed_pct, pct} — fixed % of portfolio per trade
- {type: volatility_scaled, target_risk_pct, atr_period} — scale size by volatility
RISK OVERLAY PRIMITIVES (array):
- {type: max_daily_trades, n}
- {type: max_concurrent_positions, n}
- {type: drawdown_pause, pause_pct}
- {type: session_loss_pause, max_consecutive_losses}
- {type: correlation_cap, mode, max_correlated}
"""
_SYSTEM = """You are a trading strategy architect. Design unique, personalized trading strategies by snapping primitives together like building blocks.
Rules:
- Pick exactly ONE entry primitive, configure its params thoughtfully for the user's situation
- stop_loss is always required; add creative exit combos in "other"
- Use token safety filters for meme/new token strategies (honeypot, lp_locked, etc.)
- theme = 2-3 word punchy identity (like "Momentum Engine", "Shadow Whale", "Dip Assassin")
- tagline = one sentence: the core trading philosophy of this strategy
- Make it genuinely unique — tune params to the user's specific profile, not generic defaults
- Be creative with primitive combinations — the same goal with different risk/budget should feel different
"""
_PROMPT = """\
Design a trading strategy for this user.
USER PROFILE:
{profile_json}
{extra}
PRIMITIVE CATALOG:
{catalog}
Return ONLY valid JSON (no markdown, no explanation):
{{
"theme": "2-3 word name",
"tagline": "one sentence philosophy",
"spec": {{
"meta": {{
"name": "snake_case_64chars_max",
"version": "1.0",
"risk_tier": "conservative|moderate|aggressive",
"description": "one line description",
"author_intent": "{goal}"
}},
"instrument": {{
"symbol": "TOKEN-USDC or * for dynamic",
"timeframe": "5m|15m|1H|4H|1D"
}},
"entry": {{"type": "...", ...params}},
"exit": {{
"stop_loss": {{"pct": N}},
"other": [...]
}},
"sizing": {{"type": "fixed_usd", "usd": {budget}}},
"filters": [...],
"risk_overlays": [...]
}}
}}
For dynamic/wallet/meme strategies add at top level: "universe": {{"selector": "entry_type_value", "chain": "solana"}}
"""
def _get_api_key() -> str:
"""Get API key from env or Claude OAuth credentials."""
key = os.environ.get("ANTHROPIC_API_KEY", "").strip()
if key:
return key
creds_path = os.path.expanduser("~/.claude/.credentials.json")
if not os.path.isfile(creds_path):
return ""
try:
import time
with open(creds_path) as f:
creds = json.load(f)
oauth = creds.get("claudeAiOauth", {})
token = oauth.get("accessToken", "")
if token and oauth.get("expiresAt", 0) > time.time() * 1000:
return token
except Exception:
pass
return ""
def _extract_json(text: str) -> dict[str, Any]:
# Strip any leading non-JSON characters (org prefixes, markdown fences, etc.)
text = text.strip()
# Find first { or [ — start of JSON
start = min(
(text.find(c) for c in "{[" if text.find(c) != -1),
default=0,
)
text = text[start:]
# Strip trailing markdown fences
text = re.sub(r"\s*```\s*$", "", text).strip()
return json.loads(text)
# "Other" exit types that must live in exit.other[], not directly on exit
_OTHER_EXIT_TYPES = {
"time_exit", "indicator_reversal", "smart_money_sell",
"dev_dump", "wallet_mirror_sell", "fast_dump_exit",
}
# Inner exit types that must be direct keys on exit, not in exit.other[]
_INNER_EXIT_KEYS = {"stop_loss", "take_profit", "trailing_stop", "tiered_take_profit"}
def _normalize_spec(spec: dict[str, Any]) -> dict[str, Any]:
"""
Auto-repair common LLM structural hallucinations before harness validation.
Fixes applied (all silent — no errors raised here):
1. Other-exit types placed directly on exit → moved to exit.other[]
2. Inner-exit types placed in exit.other[] → promoted to direct exit keys
3. Missing universe block when symbol == "*"
4. tiered_take_profit tiers pct_sell doesn't sum to 100 → rescale last tier
5. meta.live_only flag auto-set (harness also does this, but set it early)
"""
import copy
spec = copy.deepcopy(spec)
exit_block = spec.get("exit", {})
# Fix 1: other-exit types sitting directly on exit → move to exit.other[]
other = exit_block.get("other", [])
for key in list(exit_block.keys()):
if key in _OTHER_EXIT_TYPES:
item = exit_block.pop(key)
if isinstance(item, dict):
item.setdefault("type", key)
else:
item = {"type": key}
other.append(item)
if other:
exit_block["other"] = other
# Fix 2: inner-exit types sitting inside exit.other[] → promote to direct keys
remaining_other = []
for item in exit_block.get("other", []):
if isinstance(item, dict) and item.get("type") in _INNER_EXIT_KEYS:
key = item["type"]
promoted = {k: v for k, v in item.items() if k != "type"}
exit_block.setdefault(key, promoted)
else:
remaining_other.append(item)
if remaining_other:
exit_block["other"] = remaining_other
elif "other" in exit_block:
del exit_block["other"]
spec["exit"] = exit_block
# Fix 3: missing universe when symbol == "*"
instr = spec.get("instrument", {})
if instr.get("symbol") == "*" and "universe" not in spec:
entry_type = spec.get("entry", {}).get("type", "ranking_entry")
chain = spec.get("meta", {}).get("chain", "solana")
spec["universe"] = {"selector": entry_type, "chain": chain}
# Fix 4: tiered_take_profit pct_sell rescaling
ttp = exit_block.get("tiered_take_profit", {})
tiers = ttp.get("tiers", [])
if tiers:
total = sum(t.get("pct_sell", 0) for t in tiers)
if total != 100 and total > 0:
# Rescale all tiers proportionally, assign remainder to last
scaled = [round(t.get("pct_sell", 0) * 100 / total) for t in tiers]
diff = 100 - sum(scaled)
scaled[-1] += diff
for t, s in zip(tiers, scaled):
t["pct_sell"] = s
return spec
def generate_strategy_spec(
profile: dict[str, Any],
api_key: str = "",
) -> tuple[dict[str, Any], str, str, list[str]]:
"""
LLM-generate a unique strategy spec.
Returns (spec, theme, tagline, errors).
Empty spec + errors list means failure — caller should fall back to template.
"""
if not api_key:
api_key = _get_api_key()
if not api_key:
return {}, "", "", ["No API key — LLM generation unavailable"]
goal = profile.get("goal", "unknown")
budget = profile.get("total_budget") or profile.get("budget_per_trade") or 100
wallets = profile.get("target_wallets", [])
extra = f"\nWallets to track/mirror: {wallets}" if wallets else ""
prompt = _PROMPT.format(
profile_json=json.dumps(profile, indent=2),
catalog=PRIMITIVE_CATALOG,
goal=goal,
budget=budget,
extra=extra,
)
try:
import anthropic
from harness import validate_spec
client = anthropic.Anthropic(api_key=api_key)
messages: list[dict] = [{"role": "user", "content": prompt}]
theme = tagline = ""
spec: dict[str, Any] = {}
last_errors: list[str] = []
# Retry loop — up to 3 attempts, each feeds harness errors back to LLM
for attempt in range(3):
resp = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=2000,
system=_SYSTEM,
messages=messages,
)
raw = resp.content[0].text.strip()
try:
data = _extract_json(raw)
except json.JSONDecodeError as e:
last_errors = [f"Invalid JSON on attempt {attempt + 1}: {e}"]
messages.append({"role": "assistant", "content": raw})
messages.append({"role": "user", "content": (
f"Your response was not valid JSON. Error: {e}\n"
"Return ONLY a valid JSON object — no markdown, no explanation."
)})
continue
theme = data.get("theme", "")
tagline = data.get("tagline", "")
spec = data.get("spec", {})
if not spec or "entry" not in spec:
last_errors = ["Incomplete spec: missing 'entry' block"]
messages.append({"role": "assistant", "content": raw})
messages.append({"role": "user", "content": (
"The spec is missing the required 'entry' block. "
"Return the complete JSON spec including entry, exit, sizing, filters, risk_overlays."
)})
continue
# Normalize before validation (fix common structural errors)
spec = _normalize_spec(spec)
spec.setdefault("meta", {}).update({"theme": theme, "tagline": tagline})
ok, errors, _ = validate_spec(spec)
if ok:
return spec, theme, tagline, []
# Feed errors back for next attempt
last_errors = errors
messages.append({"role": "assistant", "content": raw})
messages.append({"role": "user", "content": (
f"The spec failed harness validation on attempt {attempt + 1}. "
f"Fix ALL of these errors and return the corrected JSON:\n"
+ "\n".join(f"- {e}" for e in errors)
)})
# All retries exhausted — return best attempt + errors
return spec, theme, tagline, last_errors
except Exception as e:
return {}, "", "", [f"LLM generation failed: {e}"]
# ── Deterministic theme fallback (when LLM unavailable) ──────────────────────
_FALLBACK_THEMES: dict[str, tuple[str, str]] = {
"meme_sniper": ("Momentum Sniper", "Catch the spike, bank in layers, flee the rug"),
"smart_money": ("Shadow Whale", "Follow alpha moves, exit when they exit"),
"copy_trade": ("Mirror Strike", "Instant mirror execution, ride their edge"),
"dca": ("Steady Stacker", "Time-based accumulation with trailing protection"),
"dip_buy": ("Dip Assassin", "Buy the fear, sell the recovery"),
"trend_follow": ("Trend Rider", "Never fight the trend, let winners run"),
"mean_revert": ("Rubber Band", "Oversold is just a discount waiting to expire"),
"grid": ("Grid Maker", "Be the market maker, collect both sides"),
}
def get_fallback_theme(goal_key: str) -> tuple[str, str]:
"""Return (theme, tagline) for when LLM is unavailable."""
return _FALLBACK_THEMES.get(goal_key, ("Custom Strategy", "Built from your unique profile"))
"""
Paper-trade graduation gate for live_only strategies.
Tracks per-strategy:
- paper_count: completed paper trades
- live_micro_count: completed live micro-trades (small size)
- days_observed: calendar days since first paper trade
- harness_breaches: number of times harness caught a violation
Unlocks full sizing when:
>= 10 paper trades + >= 5 live micro-trades + >= 7 days + 0 harness breaches
"""
from __future__ import annotations
import json
import time
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Any
GATE_DIR = Path(__file__).parent / ".paper_gate"
# Graduation thresholds
MIN_PAPER_TRADES = 10
MIN_LIVE_MICRO_TRADES = 5
MIN_DAYS_OBSERVED = 7
MAX_HARNESS_BREACHES = 0
@dataclass
class GateState:
"""Persisted state for a single strategy's graduation progress."""
strategy_name: str = ""
paper_count: int = 0
live_micro_count: int = 0
first_paper_ts: float = 0.0 # unix seconds
harness_breaches: int = 0
graduated: bool = False
graduated_ts: float = 0.0
@property
def days_observed(self) -> float:
if self.first_paper_ts == 0:
return 0.0
return (time.time() - self.first_paper_ts) / 86400
@property
def ready(self) -> bool:
return (
self.paper_count >= MIN_PAPER_TRADES
and self.live_micro_count >= MIN_LIVE_MICRO_TRADES
and self.days_observed >= MIN_DAYS_OBSERVED
and self.harness_breaches <= MAX_HARNESS_BREACHES
)
def progress_summary(self) -> dict[str, Any]:
return {
"paper_trades": f"{self.paper_count}/{MIN_PAPER_TRADES}",
"live_micro_trades": f"{self.live_micro_count}/{MIN_LIVE_MICRO_TRADES}",
"days_observed": f"{self.days_observed:.1f}/{MIN_DAYS_OBSERVED}",
"harness_breaches": self.harness_breaches,
"graduated": self.graduated,
"ready_to_graduate": self.ready,
}
def _state_path(strategy_name: str) -> Path:
GATE_DIR.mkdir(parents=True, exist_ok=True)
return GATE_DIR / f"{strategy_name}.json"
def load_state(strategy_name: str) -> GateState:
path = _state_path(strategy_name)
if not path.exists():
return GateState(strategy_name=strategy_name)
with open(path) as f:
data = json.load(f)
return GateState(**data)
def save_state(state: GateState) -> None:
path = _state_path(state.strategy_name)
with open(path, "w") as f:
json.dump(asdict(state), f, indent=2)
def record_paper_trade(strategy_name: str) -> GateState:
"""Record a completed paper trade."""
state = load_state(strategy_name)
if state.first_paper_ts == 0:
state.first_paper_ts = time.time()
state.paper_count += 1
save_state(state)
return state
def record_live_micro_trade(strategy_name: str) -> GateState:
"""Record a completed live micro-trade."""
state = load_state(strategy_name)
state.live_micro_count += 1
save_state(state)
return state
def record_harness_breach(strategy_name: str) -> GateState:
"""Record a harness violation during paper/micro trading."""
state = load_state(strategy_name)
state.harness_breaches += 1
save_state(state)
return state
def check_graduation(strategy_name: str) -> tuple[bool, dict[str, Any]]:
"""
Check if strategy is ready to graduate to full sizing.
Returns:
(graduated, progress_summary)
"""
state = load_state(strategy_name)
if state.graduated:
return True, state.progress_summary()
if state.ready:
state.graduated = True
state.graduated_ts = time.time()
save_state(state)
return True, state.progress_summary()
return False, state.progress_summary()
def get_allowed_size_multiplier(strategy_name: str) -> float:
"""
Returns sizing multiplier:
- 0.0 if no paper trades yet (blocked)
- 0.1 during micro-trade phase (10% of spec size)
- 1.0 if graduated (full size)
"""
state = load_state(strategy_name)
if state.graduated:
return 1.0
if state.paper_count >= MIN_PAPER_TRADES:
return 0.1 # micro-trade phase
return 0.0 # paper-only phase
schema_version: 1
name: starter-coach
version: "1.0.0"
description: "Starter Coach V2 — conversational 6-step skill that guides users to build their own automated DEX spot-trading bot on OKX DEX. Onboard → Profile → Build Strategy → Paper Trade → Go Live. Uses OnchainOS CLI for all on-chain data and execution. No freeform trading code — emits validated JSON strategy specs."
author:
name: "VibeCodeDaddy"
github: "VibeCodeDaddy69"
license: MIT
category: strategy
tags:
- trading-bot
- strategy-builder
- coaching
- paper-trade
- backtesting
- dex
- onchainos
- solana
- ethereum
components:
skill:
dir: .
api_calls: []
type: community-developer
"""Starter Coach V2 — Primitive evaluators."""
"""
Entry trigger evaluators — 12 primitives.
Each function: evaluate_<type>(ctx, params) -> bool
ctx: MarketContext with OHLCV bars, indicators, OnchainOS data
params: dict from the spec's entry block (minus the "type" key)
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
import time as _time
from onchainos import OnchainOS, WalletEvent, RankingItem
@dataclass
class Bar:
ts: float # unix seconds
open: float
high: float
low: float
close: float
volume: float
@dataclass
class MarketContext:
"""Shared context passed to all evaluators."""
bars: list[Bar] = field(default_factory=list) # newest last
current_price: float = 0.0
timeframe: str = "5m"
chain: str = "solana"
token: str = ""
# Pre-computed indicators (populated by engine before eval)
ema: dict[int, list[float]] = field(default_factory=dict) # period -> values
sma: dict[int, list[float]] = field(default_factory=dict)
rsi: dict[int, list[float]] = field(default_factory=dict) # period -> values
macd: dict[str, list[float]] = field(default_factory=dict) # "macd"/"signal"/"hist"
bbands: dict[str, list[float]] = field(default_factory=dict) # "upper"/"middle"/"lower"
# OnchainOS live data (populated for live_only triggers)
onchainos: OnchainOS | None = None
wallet_events: list[WalletEvent] = field(default_factory=list)
ranking_items: list[RankingItem] = field(default_factory=list)
# ── Backtestable entries ─────────────────────────────────────────────────────
def evaluate_price_drop(ctx: MarketContext, params: dict) -> bool:
"""E-01: Fire when price drops pct% from lookback high."""
pct = params["pct"]
lookback = params["lookback_bars"]
if len(ctx.bars) < lookback:
return False
window = ctx.bars[-lookback:]
high = max(b.high for b in window)
if high == 0:
return False
drop = (high - ctx.current_price) / high * 100
return drop >= pct
def evaluate_price_breakout(ctx: MarketContext, params: dict) -> bool:
"""E-02: Fire when price breaks above lookback high (or below low)."""
direction = params["direction"]
lookback = params["lookback_bars"]
confirm_pct = params.get("confirm_pct", 0)
if len(ctx.bars) < lookback + 1:
return False
window = ctx.bars[-(lookback + 1):-1] # exclude current bar
if direction == "up":
level = max(b.high for b in window)
threshold = level * (1 + confirm_pct / 100)
return ctx.current_price > threshold
else:
level = min(b.low for b in window)
threshold = level * (1 - confirm_pct / 100)
return ctx.current_price < threshold
def evaluate_ma_cross(ctx: MarketContext, params: dict) -> bool:
"""E-03: Fire when fast MA crosses above slow MA (golden cross)."""
fast_p = params["fast_period"]
slow_p = params["slow_period"]
ma_type = params.get("ma_type", "EMA")
source = ctx.ema if ma_type == "EMA" else ctx.sma
fast = source.get(fast_p, [])
slow = source.get(slow_p, [])
if len(fast) < 2 or len(slow) < 2:
return False
# Cross: prev fast <= slow, now fast > slow
return fast[-2] <= slow[-2] and fast[-1] > slow[-1]
def evaluate_rsi_threshold(ctx: MarketContext, params: dict) -> bool:
"""E-04: Fire when RSI crosses above/below a level."""
period = params["period"]
level = params["level"]
direction = params["direction"]
rsi = ctx.rsi.get(period, [])
if len(rsi) < 2:
return False
if direction == "cross_up":
return rsi[-2] <= level and rsi[-1] > level
else: # cross_down
return rsi[-2] >= level and rsi[-1] < level
def evaluate_volume_spike(ctx: MarketContext, params: dict) -> bool:
"""E-05: Fire when current bar volume > multiplier * rolling average."""
multiplier = params["multiplier"]
avg_bars = params["avg_bars"]
if len(ctx.bars) < avg_bars + 1:
return False
avg_window = ctx.bars[-(avg_bars + 1):-1]
avg_vol = sum(b.volume for b in avg_window) / len(avg_window)
if avg_vol == 0:
return False
return ctx.bars[-1].volume > multiplier * avg_vol
def evaluate_time_schedule(ctx: MarketContext, params: dict) -> bool:
"""E-06: Fire on fixed cadence. Engine calls this once per interval tick."""
# The engine is responsible for calling this at the right time
# based on interval and anchor_utc. When called, it always fires.
return True
def evaluate_macd_cross(ctx: MarketContext, params: dict) -> bool:
"""E-09: Fire when MACD line crosses signal line."""
direction = params["direction"]
macd_line = ctx.macd.get("macd", [])
signal_line = ctx.macd.get("signal", [])
if len(macd_line) < 2 or len(signal_line) < 2:
return False
if direction == "cross_up":
return macd_line[-2] <= signal_line[-2] and macd_line[-1] > signal_line[-1]
else:
return macd_line[-2] >= signal_line[-2] and macd_line[-1] < signal_line[-1]
def evaluate_bollinger_touch(ctx: MarketContext, params: dict) -> bool:
"""E-10: Fire when price touches upper or lower Bollinger Band."""
band = params["band"]
band_key = band # "upper" or "lower"
bb = ctx.bbands.get(band_key, [])
if not bb:
return False
if band == "lower":
return ctx.current_price <= bb[-1]
else:
return ctx.current_price >= bb[-1]
# ── Live-only entries ────────────────────────────────────────────────────────
def evaluate_smart_money_buy(ctx: MarketContext, params: dict) -> bool:
"""E-07: Fire when >= N smart-money wallets buy within window. Live-only."""
min_wallets = params["min_wallets"]
window_min = params["window_min"]
min_usd = params.get("min_usd_each", 0)
now = _time.time()
cutoff = now - window_min * 60
sm_buys: set[str] = set()
for ev in ctx.wallet_events:
if ev.side == "buy" and ev.timestamp >= cutoff and ev.usd_amount >= min_usd:
sm_buys.add(ev.wallet)
return len(sm_buys) >= min_wallets
def evaluate_dev_buy(ctx: MarketContext, params: dict) -> bool:
"""E-08: Fire when deployer wallet buys above threshold. Live-only."""
min_usd = params["min_usd"]
window_min = params.get("window_min", 1440)
now = _time.time()
cutoff = now - window_min * 60
for ev in ctx.wallet_events:
if ev.side == "buy" and ev.timestamp >= cutoff and ev.usd_amount >= min_usd:
# Engine must pre-filter wallet_events to dev wallet only
return True
return False
def evaluate_ranking_entry(ctx: MarketContext, params: dict) -> bool:
"""E-11: Fire when token enters top-N of ranking list. Live-only."""
top_n = params["top_n"]
for item in ctx.ranking_items:
if item.rank <= top_n:
return True
return False
def evaluate_wallet_copy_buy(ctx: MarketContext, params: dict) -> bool:
"""E-12: Fire when tracked wallet buys above threshold. Live-only."""
min_usd = params["min_usd"]
for ev in ctx.wallet_events:
if ev.side == "buy" and ev.usd_amount >= min_usd:
return True
return False
# ── Dispatcher ───────────────────────────────────────────────────────────────
ENTRY_EVALUATORS: dict[str, Any] = {
"price_drop": evaluate_price_drop,
"price_breakout": evaluate_price_breakout,
"ma_cross": evaluate_ma_cross,
"rsi_threshold": evaluate_rsi_threshold,
"volume_spike": evaluate_volume_spike,
"time_schedule": evaluate_time_schedule,
"smart_money_buy": evaluate_smart_money_buy,
"dev_buy": evaluate_dev_buy,
"macd_cross": evaluate_macd_cross,
"bollinger_touch": evaluate_bollinger_touch,
"ranking_entry": evaluate_ranking_entry,
"wallet_copy_buy": evaluate_wallet_copy_buy,
}
def evaluate_entry(ctx: MarketContext, entry_spec: dict) -> bool:
"""Dispatch to the correct entry evaluator."""
entry_type = entry_spec["type"]
params = {k: v for k, v in entry_spec.items() if k != "type"}
evaluator = ENTRY_EVALUATORS.get(entry_type)
if evaluator is None:
raise ValueError(f"Unknown entry type: {entry_type}")
return evaluator(ctx, params)
"""
Risk overlay evaluators — 5 primitives.
Each function: check_overlay(state, params) -> bool
True = OK to proceed, False = blocked by overlay.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
import time as _time
@dataclass
class PortfolioState:
"""Tracked by the engine across trades."""
trades_today: int = 0
open_positions: int = 0
open_tokens: list[str] = field(default_factory=list)
equity_usd: float = 0.0
peak_equity_usd: float = 0.0
consecutive_losses: int = 0
session_start_ts: float = 0.0
def check_max_daily_trades(state: PortfolioState, params: dict) -> bool:
"""R-01: Hard cap on entries per 24h."""
return state.trades_today < params["n"]
def check_max_concurrent_positions(state: PortfolioState, params: dict) -> bool:
"""R-02: Max open positions at once."""
return state.open_positions < params["n"]
def check_drawdown_pause(state: PortfolioState, params: dict) -> bool:
"""R-03: Pause if equity drawdown exceeds threshold."""
pause_pct = params["pause_pct"]
if state.peak_equity_usd == 0:
return True
dd = (state.peak_equity_usd - state.equity_usd) / state.peak_equity_usd * 100
return dd < pause_pct
def check_correlation_cap(state: PortfolioState, params: dict) -> bool:
"""R-04: v1.0 same_token_dedupe only. Prevent duplicate token positions."""
max_corr = params["max_correlated"]
# Count how many open positions share the same token
from collections import Counter
counts = Counter(state.open_tokens)
for token, count in counts.items():
if count >= max_corr:
return False
return True
def check_session_loss_pause(state: PortfolioState, params: dict) -> bool:
"""R-05: Pause after N consecutive losses."""
max_losses = params["max_consecutive_losses"]
session_hours = params.get("session_hours", 24)
# Check if we're still in the session window
now = _time.time()
if now - state.session_start_ts > session_hours * 3600:
# Session expired, reset
state.consecutive_losses = 0
state.session_start_ts = now
return True
return state.consecutive_losses < max_losses
RISK_EVALUATORS: dict[str, Any] = {
"max_daily_trades": check_max_daily_trades,
"max_concurrent_positions": check_max_concurrent_positions,
"drawdown_pause": check_drawdown_pause,
"correlation_cap": check_correlation_cap,
"session_loss_pause": check_session_loss_pause,
}
def check_all_overlays(
state: PortfolioState, overlays: list[dict]
) -> tuple[bool, list[str]]:
"""
Check all risk overlays. ALL must pass.
Returns (all_pass, list_of_blocking_overlay_types).
"""
blockers: list[str] = []
for overlay in overlays:
otype = overlay.get("type", "")
checker = RISK_EVALUATORS.get(otype)
if checker is None:
blockers.append(f"unknown:{otype}")
continue
params = {k: v for k, v in overlay.items() if k != "type"}
if not checker(state, params):
blockers.append(otype)
return len(blockers) == 0, blockers
"""
Sizing evaluators — 3 primitives.
Each function: compute_size(equity_usd, ctx, params) -> float (USD to trade)
L3 hard bound: max 10% of equity per trade.
"""
from __future__ import annotations
from typing import Any
from primitives.entry import MarketContext
L3_MAX_PCT = 10.0 # hard bound
def compute_fixed_pct(equity_usd: float, ctx: MarketContext, params: dict) -> float:
"""S-01: Fixed % of current equity."""
pct = min(params["pct"], L3_MAX_PCT)
return equity_usd * pct / 100
def compute_fixed_usd(equity_usd: float, ctx: MarketContext, params: dict) -> float:
"""S-02: Fixed USD amount."""
usd = params["usd"]
max_allowed = equity_usd * L3_MAX_PCT / 100
return min(usd, max_allowed)
def compute_volatility_scaled(equity_usd: float, ctx: MarketContext, params: dict) -> float:
"""S-03: Size inversely scaled by ATR. Smaller in volatile markets."""
target_risk = params["target_risk_pct"]
atr_period = params.get("atr_period", 14)
if len(ctx.bars) < atr_period + 1:
return 0.0
# Compute ATR
trs: list[float] = []
for i in range(-atr_period, 0):
bar = ctx.bars[i]
prev = ctx.bars[i - 1]
tr = max(bar.high - bar.low, abs(bar.high - prev.close), abs(bar.low - prev.close))
trs.append(tr)
atr = sum(trs) / len(trs)
if atr == 0 or ctx.current_price == 0:
return 0.0
atr_pct = atr / ctx.current_price * 100
# Position size = (target_risk% of equity) / ATR%
raw_size = (equity_usd * target_risk / 100) / (atr_pct / 100)
max_allowed = equity_usd * L3_MAX_PCT / 100
return min(raw_size, max_allowed)
SIZING_EVALUATORS: dict[str, Any] = {
"fixed_pct": compute_fixed_pct,
"fixed_usd": compute_fixed_usd,
"volatility_scaled": compute_volatility_scaled,
}
def compute_size(equity_usd: float, ctx: MarketContext, sizing_spec: dict) -> float:
"""Dispatch to the correct sizing function."""
stype = sizing_spec["type"]
params = {k: v for k, v in sizing_spec.items() if k != "type"}
fn = SIZING_EVALUATORS.get(stype)
if fn is None:
raise ValueError(f"Unknown sizing type: {stype}")
size = fn(equity_usd, ctx, params)
# Final L3 clamp
max_allowed = equity_usd * L3_MAX_PCT / 100
return max(0.0, min(size, max_allowed))
"""Starter Coach V2 — Meta-templates."""