From 6c6c02076bcc58ae78cb0ba681dacb6b2f852b4f Mon Sep 17 00:00:00 2001 From: rapcmia Date: Tue, 28 Jul 2026 23:25:05 +0800 Subject: [PATCH 1/6] feat(agents): add adaptive grid trader --- agents/adaptive_grid_trader/AGENT.md | 93 ++++++++ .../routines/baseline_7d.py | 160 +++++++++++++ .../routines/hourly_mtf_check.py | 211 ++++++++++++++++++ .../routines/order_size_validator.py | 169 ++++++++++++++ 4 files changed, 633 insertions(+) create mode 100644 agents/adaptive_grid_trader/AGENT.md create mode 100644 agents/adaptive_grid_trader/routines/baseline_7d.py create mode 100644 agents/adaptive_grid_trader/routines/hourly_mtf_check.py create mode 100644 agents/adaptive_grid_trader/routines/order_size_validator.py diff --git a/agents/adaptive_grid_trader/AGENT.md b/agents/adaptive_grid_trader/AGENT.md new file mode 100644 index 00000000..91fafb37 --- /dev/null +++ b/agents/adaptive_grid_trader/AGENT.md @@ -0,0 +1,93 @@ +--- +name: Adaptive Grid Trader +description: Expert in multi-timeframe adaptive grid trading with safety-first order + sizing, 20% reserve requirement, and strict risk management +agent_key: openrouter:anthropic/claude-sonnet-4.5 +tools: +- get_market_data +- get_portfolio_overview +- manage_executors +- search_history +- manage_routines +- trading_agent_journal_read +- trading_agent_journal_write +when_to_consult: When the user wants to deploy, configure, monitor, or refine an adaptive + grid trading strategy that auto-adjusts direction based on market conditions and + enforces safe order sizing with exchange compliance +server_required: true +server_name: '' +created_by: 1474408604 +created_at: '2026-07-28T14:49:09.946902+00:00' +--- + +# Adaptive Grid Trader + +You are an expert in **adaptive grid trading** — deploying directional grids (LONG/SHORT/TWO_SIDED) that adjust based on multi-timeframe market analysis, with safety-first order sizing and strict risk management. + +## What you DO + +- **Multi-timeframe market analysis**: Analyze 7d baseline, then hourly 1h/6h/12h checks to choose grid direction (LONG_GRID, SHORT_GRID, TWO_SIDED_GRID, or HOLD) +- **Safety-first order sizing**: Enforce 20% wallet reserve, compare user preference vs. exchange minimum, suggest buffered order size with user approval +- **Grid construction**: Calculate how many valid orders fit within budget, ensure each order ≥ max(user_preference, exchange_minimum) +- **Risk management**: Configure leverage (3x-10x range), hard stop-loss, trailing stop, and emergency shutdown protocol +- **Position verification**: Cancel all orders, close position with reduce-only, verify position=0, retry with alerts if anything remains + +## What you do NOT handle + +- Non-grid strategies (DCA, market making, position executors without grid structure) +- Manual order placement outside grid framework +- Backtesting (defer to controller configs and backtest tools) + +## Core Logic + +### Pre-Trade Safety Checks +1. Read wallet balance +2. Require Trading Budget + 20% reserve (e.g., 100 USDT budget → need 120 USDT available) +3. Check exchange minimum order size in background +4. Compare user's preferred minimum vs. exchange minimum +5. Suggest buffered order size (e.g., if both are 5 USDT, suggest 6 USDT) +6. Get user approval before using larger size (unless auto-approve flag enabled) + +### Market Decision Flow +- **Initial**: Analyze previous 7 days as baseline +- **Hourly**: Analyze 1h, 6h, 12h data +- **Choose**: LONG_GRID, SHORT_GRID, TWO_SIDED_GRID (only on hedge-mode exchanges), or HOLD +- **Anti-flip rule**: Don't quickly flip long ↔ short unless 6h and 12h confirm, except emergency exits + +### Grid Rules +- Calculate how many valid orders fit within Trading Budget +- No order below max(user_preference, exchange_minimum) +- If no safe valid grid can be built → HOLD +- Never start new grid until old position fully closed + +### Risk & Shutdown +- **Leverage**: 3x-5x for dry run, 3x-10x design range +- **Hard stop-loss**: Protect total grid loss +- **Trailing stop**: Protect profit after grid becomes profitable +- **Emergency shutdown protocol**: + 1. Cancel all remaining grid orders + 2. Close remaining position (reduce-only) + 3. Verify position size = 0 + 4. Retry safely and alert if anything remains + 5. Never start another grid until old position fully closed + +## How you answer + +- **Lead with the recommendation** (action, direction, order size, leverage) +- **Key: value format**, not prose +- **Show your work**: what timeframes say, what the safety check found, why HOLD vs. trade +- **Before any trade**: confirm user has approved the specific parameters (pair, budget, order size, leverage) +- **On errors**: read the failure, explain in plain terms, propose fix + +## Memory & Skills + +You own domain memory (market learnings, user preferences for this strategy) and reusable skills (e.g., "how to size orders with buffer", "emergency shutdown checklist"). Use `manage_memory` and `manage_skill` to refine your judgment over time. + +## Routines (to be added) + +You will call analysis routines by name: +- `baseline_7d`: Initial 7-day market analysis +- `hourly_mtf_check`: 1h/6h/12h multi-timeframe analysis +- `order_size_validator`: Check exchange minimums and suggest buffer + +When running on a loop, call these routines, read their output, and decide. diff --git a/agents/adaptive_grid_trader/routines/baseline_7d.py b/agents/adaptive_grid_trader/routines/baseline_7d.py new file mode 100644 index 00000000..c72c2515 --- /dev/null +++ b/agents/adaptive_grid_trader/routines/baseline_7d.py @@ -0,0 +1,160 @@ +from pydantic import BaseModel, Field +from telegram.ext import ContextTypes +from config_manager import get_client +import logging +import statistics + +logger = logging.getLogger(__name__) + +CATEGORY = "Analysis" + + +class Config(BaseModel): + """7-day baseline analysis: trend direction, support/resistance levels, and ATR volatility for the adaptive grid trader.""" + + connector_name: str = Field(default="binance_perpetual", description="Exchange connector name") + trading_pair: str = Field(default="BTC-USDT", description="Trading pair to analyze") + lookback_days: int = Field(default=7, description="Number of days to look back (uses 4h candles)") + + +async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: + client = await get_client(context._chat_id, context=context) + if not client: + return "No server available" + + try: + max_records = config.lookback_days * 6 + 10 # 6 four-hour candles per day + result = await client.market_data.get_candles( + config.connector_name, config.trading_pair, interval="4h", max_records=max_records + ) + records = result if isinstance(result, list) else result.get("data", result.get("candles", [])) + + if not records or len(records) < 5: + return f"Insufficient candle data for {config.trading_pair} on {config.connector_name}" + + closes = [float(c["close"]) for c in records] + highs = [float(c["high"]) for c in records] + lows = [float(c["low"]) for c in records] + current_price = closes[-1] + + # Trend: SMA short vs SMA long + sma_short = statistics.mean(closes[-7:]) + sma_long = statistics.mean(closes[-21:]) if len(closes) >= 21 else statistics.mean(closes) + + if sma_short > sma_long * 1.001: + trend_direction = "up" + elif sma_short < sma_long * 0.999: + trend_direction = "down" + else: + trend_direction = "range" + + # ATR-based volatility + true_ranges = [] + for i in range(1, len(records)): + high = float(records[i]["high"]) + low = float(records[i]["low"]) + prev_close = float(records[i - 1]["close"]) + tr = max(high - low, abs(high - prev_close), abs(low - prev_close)) + true_ranges.append(tr) + + atr_period = min(14, len(true_ranges)) + atr = statistics.mean(true_ranges[-atr_period:]) + volatility_pct = (atr / current_price) * 100 + + # Support/resistance via swing highs/lows + window = 3 + resistance_levels = [] + support_levels = [] + for i in range(window, len(highs) - window): + if highs[i] == max(highs[i - window : i + window + 1]): + resistance_levels.append(highs[i]) + if lows[i] == min(lows[i - window : i + window + 1]): + support_levels.append(lows[i]) + + resistance_levels.sort(key=lambda x: abs(x - current_price)) + support_levels.sort(key=lambda x: abs(x - current_price)) + + key_levels = [] + for level in resistance_levels[:3]: + key_levels.append( + { + "level": round(level, 4), + "type": "resistance", + "distance_pct": round((level - current_price) / current_price * 100, 2), + } + ) + for level in support_levels[:3]: + key_levels.append( + { + "level": round(level, 4), + "type": "support", + "distance_pct": round((level - current_price) / current_price * 100, 2), + } + ) + key_levels.sort(key=lambda x: abs(x["distance_pct"])) + + period_high = max(highs) + period_low = min(lows) + range_pct = (period_high - period_low) / period_low * 100 + + # Report + try: + from condor.reports import ReportBuilder + + builder = ReportBuilder(f"{config.lookback_days}d Baseline: {config.trading_pair}") + builder.source("routine", "baseline_7d").tags(["analysis", "grid", "baseline"]) + + builder.section("01 / TREND", f"{config.lookback_days}d of 4h candles — SMA cross signal") + builder.kpi("Trend Direction", trend_direction.upper()) + builder.kpi("Current Price", f"{current_price:,.4f}") + builder.kpi("SMA Short (7c)", f"{sma_short:,.4f}") + builder.kpi("SMA Long (21c)", f"{sma_long:,.4f}") + + builder.section("02 / VOLATILITY", "ATR (14-period) on 4h candles") + builder.kpi("ATR (14-period)", f"{atr:,.4f}") + builder.kpi("Volatility %", f"{volatility_pct:.2f}%") + builder.kpi(f"{config.lookback_days}d High", f"{period_high:,.4f}") + builder.kpi(f"{config.lookback_days}d Low", f"{period_low:,.4f}") + builder.kpi("Price Range %", f"{range_pct:.2f}%") + + builder.section("03 / KEY LEVELS", "Nearest swing-high resistances and swing-low supports") + if key_levels: + builder.table( + [ + { + "Level": f"{level['level']:,.4f}", + "Type": level["type"].capitalize(), + "Distance %": f"{level['distance_pct']:+.2f}%", + } + for level in key_levels + ], + ["Level", "Type", "Distance %"], + ) + else: + builder.markdown("_No significant swing levels detected in this window._") + + builder.manual_order() + await builder.save() + except Exception as e: + logger.warning(f"Report generation failed: {e}") + + from routines.base import RoutineResult + + return RoutineResult( + text=( + f"{config.trading_pair} {config.lookback_days}d Baseline | " + f"Trend: **{trend_direction.upper()}** | " + f"Volatility: {volatility_pct:.2f}% | " + f"Key levels: {len(key_levels)}" + ), + sections=[ + {"type": "kpi", "label": "Trend Direction", "value": trend_direction.upper()}, + {"type": "kpi", "label": "Volatility %", "value": f"{volatility_pct:.2f}%"}, + {"type": "kpi", "label": "ATR", "value": f"{atr:,.4f}"}, + {"type": "kpi", "label": "Key Levels", "value": str(len(key_levels))}, + ], + ) + + except Exception as e: + logger.error(f"baseline_7d error: {e}") + return f"Error running baseline analysis: {e}" diff --git a/agents/adaptive_grid_trader/routines/hourly_mtf_check.py b/agents/adaptive_grid_trader/routines/hourly_mtf_check.py new file mode 100644 index 00000000..96a8ee18 --- /dev/null +++ b/agents/adaptive_grid_trader/routines/hourly_mtf_check.py @@ -0,0 +1,211 @@ +from pydantic import BaseModel, Field +from telegram.ext import ContextTypes +from config_manager import get_client +import logging +import statistics +import asyncio + +logger = logging.getLogger(__name__) + +CATEGORY = "Analysis" + + +class Config(BaseModel): + """Multi-timeframe grid direction check (1h/6h/12h). Recommends LONG_GRID/SHORT_GRID/TWO_SIDED_GRID/HOLD with confidence 1-5.""" + + connector_name: str = Field(default="binance_perpetual", description="Exchange connector name") + trading_pair: str = Field(default="BTC-USDT", description="Trading pair to analyze") + short_tf: str = Field(default="1h", description="Short timeframe interval") + mid_tf: str = Field(default="6h", description="Mid timeframe interval") + long_tf: str = Field(default="12h", description="Long timeframe interval") + rsi_period: int = Field(default=14, description="RSI calculation period") + + +def _momentum_roc(closes: list, period: int = 14) -> float: + """Rate of change momentum: (current - n_ago) / n_ago * 100.""" + if len(closes) < period + 1: + return 0.0 + return (closes[-1] - closes[-period]) / closes[-period] * 100 + + +def _rsi(closes: list, period: int = 14) -> float: + """Simple RSI.""" + if len(closes) < period + 1: + return 50.0 + gains, losses = [], [] + for i in range(1, len(closes)): + diff = closes[i] - closes[i - 1] + gains.append(max(diff, 0.0)) + losses.append(max(-diff, 0.0)) + avg_gain = statistics.mean(gains[-period:]) + avg_loss = statistics.mean(losses[-period:]) + if avg_loss == 0: + return 100.0 + return 100 - (100 / (1 + avg_gain / avg_loss)) + + +async def _fetch(client, connector: str, pair: str, interval: str, records: int) -> list: + result = await client.market_data.get_candles(connector, pair, interval=interval, max_records=records) + return result if isinstance(result, list) else result.get("data", result.get("candles", [])) + + +async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: + client = await get_client(context._chat_id, context=context) + if not client: + return "No server available" + + try: + raw_short, raw_mid, raw_long = await asyncio.gather( + _fetch(client, config.connector_name, config.trading_pair, config.short_tf, 50), + _fetch(client, config.connector_name, config.trading_pair, config.mid_tf, 30), + _fetch(client, config.connector_name, config.trading_pair, config.long_tf, 20), + ) + + tf_configs = [ + (config.short_tf, raw_short), + (config.mid_tf, raw_mid), + (config.long_tf, raw_long), + ] + + tf_data = {} + for tf_name, raw in tf_configs: + if not raw or len(raw) < 5: + continue + closes = [float(c["close"]) for c in raw] + sma_short = statistics.mean(closes[-5:]) + sma_long = statistics.mean(closes) + mom = _momentum_roc(closes, period=min(config.rsi_period, len(closes) - 1)) + rsi = _rsi(closes, period=config.rsi_period) + + if sma_short > sma_long * 1.001: + trend = "bullish" + elif sma_short < sma_long * 0.999: + trend = "bearish" + else: + trend = "neutral" + + tf_data[tf_name] = { + "closes": closes, + "momentum": mom, + "rsi": rsi, + "current": closes[-1], + "trend": trend, + "sma_short": sma_short, + "sma_long": sma_long, + } + + if not tf_data: + return "Could not fetch candle data for any timeframe" + + # Alignment scoring + bullish_count = sum(1 for d in tf_data.values() if d["trend"] == "bullish") + bearish_count = sum(1 for d in tf_data.values() if d["trend"] == "bearish") + total = len(tf_data) + + # Divergence: momentum sign differs across timeframes + mom_signs = [1 if d["momentum"] >= 0 else -1 for d in tf_data.values()] + has_divergence = len(set(mom_signs)) > 1 + + # RSI overbought/oversold flags + rsi_flags = [] + for tf_name, d in tf_data.items(): + if d["rsi"] > 70: + rsi_flags.append(f"{tf_name} RSI overbought ({d['rsi']:.1f})") + elif d["rsi"] < 30: + rsi_flags.append(f"{tf_name} RSI oversold ({d['rsi']:.1f})") + + # Recommendation logic + if bullish_count == total: + recommendation = "LONG_GRID" + confidence = 5 if not has_divergence else 4 + reasoning = f"All {total} timeframes bullish" + elif bearish_count == total: + recommendation = "SHORT_GRID" + confidence = 5 if not has_divergence else 4 + reasoning = f"All {total} timeframes bearish" + elif bullish_count > bearish_count: + recommendation = "LONG_GRID" + confidence = 4 if not has_divergence else 3 + reasoning = f"{bullish_count}/{total} timeframes bullish" + elif bearish_count > bullish_count: + recommendation = "SHORT_GRID" + confidence = 4 if not has_divergence else 3 + reasoning = f"{bearish_count}/{total} timeframes bearish" + elif has_divergence: + recommendation = "HOLD" + confidence = 2 + reasoning = "Mixed signals with momentum divergence — unsafe to commit direction" + else: + recommendation = "TWO_SIDED_GRID" + confidence = 3 + reasoning = "Balanced momentum across timeframes — ranging/neutral conditions" + + if rsi_flags: + reasoning += f"; {', '.join(rsi_flags)}" + + divergence_list = [] + if has_divergence: + divergence_list.append("Momentum direction divergence across timeframes") + divergence_list.extend(rsi_flags) + + tf_table = [ + { + "Timeframe": tf_name, + "Trend": d["trend"].capitalize(), + "Momentum": f"{d['momentum']:+.2f}%", + "RSI": f"{d['rsi']:.1f}", + "SMA Short": f"{d['sma_short']:,.4f}", + "SMA Long": f"{d['sma_long']:,.4f}", + } + for tf_name, d in tf_data.items() + ] + + # Report + try: + from condor.reports import ReportBuilder + + builder = ReportBuilder(f"MTF Grid Check: {config.trading_pair}") + builder.source("routine", "hourly_mtf_check").tags(["analysis", "grid", "mtf"]) + + builder.section("01 / RECOMMENDATION") + builder.kpi("Recommendation", recommendation) + builder.kpi("Confidence", f"{confidence} / 5") + builder.kpi("Bullish TFs", f"{bullish_count}/{total}") + builder.kpi("Bearish TFs", f"{bearish_count}/{total}") + + builder.section( + "02 / TIMEFRAME BREAKDOWN", + f"Timeframes analyzed: {config.short_tf}, {config.mid_tf}, {config.long_tf}", + ) + builder.table(tf_table, ["Timeframe", "Trend", "Momentum", "RSI", "SMA Short", "SMA Long"]) + + if divergence_list: + builder.section("03 / DIVERGENCES & FLAGS") + builder.markdown("\n".join(f"- {d}" for d in divergence_list)) + + builder.section("04 / REASONING") + builder.markdown(reasoning) + + builder.manual_order() + await builder.save() + except Exception as e: + logger.warning(f"Report generation failed: {e}") + + from routines.base import RoutineResult + + return RoutineResult( + text=( + f"{config.trading_pair} MTF Check → **{recommendation}** " + f"(confidence {confidence}/5) | {reasoning}" + ), + sections=[ + {"type": "kpi", "label": "Recommendation", "value": recommendation}, + {"type": "kpi", "label": "Confidence", "value": f"{confidence}/5"}, + {"type": "kpi", "label": "Bullish TFs", "value": f"{bullish_count}/{total}"}, + {"type": "kpi", "label": "Bearish TFs", "value": f"{bearish_count}/{total}"}, + ], + ) + + except Exception as e: + logger.error(f"hourly_mtf_check error: {e}") + return f"Error running MTF check: {e}" diff --git a/agents/adaptive_grid_trader/routines/order_size_validator.py b/agents/adaptive_grid_trader/routines/order_size_validator.py new file mode 100644 index 00000000..76f82c60 --- /dev/null +++ b/agents/adaptive_grid_trader/routines/order_size_validator.py @@ -0,0 +1,169 @@ +from pydantic import BaseModel, Field +from telegram.ext import ContextTypes +from config_manager import get_client +import logging + +logger = logging.getLogger(__name__) + +CATEGORY = "Monitoring" + + +class Config(BaseModel): + """Validates order size against exchange minimums and recommends a buffered safe size. Checks compliance and suggests 10-20% buffer above exchange minimum.""" + + connector_name: str = Field(default="binance_perpetual", description="Exchange connector name") + trading_pair: str = Field(default="BTC-USDT", description="Trading pair to validate") + exchange_min_qty: float = Field( + default=0.001, description="Exchange minimum order quantity in base asset (check exchange docs)" + ) + user_preferred_qty: float = Field( + default=0.001, description="User's intended minimum order quantity in base asset" + ) + buffer_pct: float = Field( + default=20.0, description="Safety buffer % above exchange minimum (10-20 recommended)" + ) + + +async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: + client = await get_client(context._chat_id, context=context) + if not client: + return "No server available" + + try: + # Fetch current price for notional calculations + current_price = 0.0 + try: + prices = await client.market_data.get_prices( + config.connector_name, trading_pairs=[config.trading_pair] + ) + current_price = float(prices.get(config.trading_pair, 0.0)) + except Exception: + pass + + if current_price <= 0: + try: + ob = await client.market_data.get_order_book( + config.connector_name, config.trading_pair, depth=1 + ) + bid = ob["bids"][0][0] if ob.get("bids") else 0 + ask = ob["asks"][0][0] if ob.get("asks") else 0 + current_price = (float(bid) + float(ask)) / 2 if bid and ask else 0.0 + except Exception: + pass + + exchange_min = config.exchange_min_qty + user_pref = config.user_preferred_qty + buffer = config.buffer_pct / 100.0 + + buffered_min = exchange_min * (1 + buffer) + recommended_size = max(buffered_min, user_pref) + + # Compliance flags + is_compliant = user_pref >= exchange_min + meets_buffer = user_pref >= buffered_min + + # Status + if not is_compliant: + status = "FAIL" + status_detail = f"User preference ({user_pref:.6f}) is BELOW exchange minimum ({exchange_min:.6f})" + elif not meets_buffer: + status = "WARN" + status_detail = f"User preference meets exchange minimum but is below {config.buffer_pct:.0f}% buffer ({buffered_min:.6f})" + else: + status = "OK" + status_detail = f"User preference meets the {config.buffer_pct:.0f}% buffered minimum" + + def notional(qty: float) -> str: + return f"${qty * current_price:,.2f}" if current_price > 0 else "N/A" + + rows = [ + { + "Parameter": "Exchange Minimum", + "Qty": f"{exchange_min:.6f}", + "Notional (USD)": notional(exchange_min), + "Note": "Hard floor — orders below this are rejected", + }, + { + "Parameter": f"Buffered Minimum (+{config.buffer_pct:.0f}%)", + "Qty": f"{buffered_min:.6f}", + "Notional (USD)": notional(buffered_min), + "Note": "Recommended floor for safe operation", + }, + { + "Parameter": "User Preference", + "Qty": f"{user_pref:.6f}", + "Notional (USD)": notional(user_pref), + "Note": "✓ Compliant" if is_compliant else "✗ Below exchange minimum", + }, + { + "Parameter": "Recommended Size", + "Qty": f"{recommended_size:.6f}", + "Notional (USD)": notional(recommended_size), + "Note": f"max(buffered_min, user_pref) with {config.buffer_pct:.0f}% buffer", + }, + ] + + # Report + try: + from condor.reports import ReportBuilder + + builder = ReportBuilder(f"Order Size Validator: {config.trading_pair}") + builder.source("routine", "order_size_validator").tags(["monitoring", "grid", "order-size"]) + + builder.section("01 / VALIDATION RESULT") + builder.kpi("Status", status) + builder.kpi("Current Price", f"${current_price:,.4f}" if current_price > 0 else "N/A") + builder.kpi("Exchange Min Qty", f"{exchange_min:.6f}") + builder.kpi("Buffered Min Qty", f"{buffered_min:.6f}") + builder.kpi("Recommended Qty", f"{recommended_size:.6f}") + builder.kpi("Buffer Applied", f"{config.buffer_pct:.0f}%") + + builder.section("02 / SIZE COMPARISON TABLE") + builder.table(rows, ["Parameter", "Qty", "Notional (USD)", "Note"]) + + if status == "FAIL": + builder.section("03 / ACTION REQUIRED") + builder.markdown( + f"**User preference ({user_pref:.6f}) is below the exchange minimum ({exchange_min:.6f}).**\n\n" + f"Orders at this size will be rejected by {config.connector_name}. " + f"Set your minimum order quantity to at least **{recommended_size:.6f}** " + f"(exchange min + {config.buffer_pct:.0f}% buffer)." + ) + elif status == "WARN": + builder.section("03 / RECOMMENDATION") + builder.markdown( + f"User preference passes the exchange minimum but sits below the {config.buffer_pct:.0f}% safety buffer.\n\n" + f"Consider using **{buffered_min:.6f}** or higher to reduce the risk of " + f"edge-case rejections due to rounding or fee deductions." + ) + else: + builder.section("03 / SUMMARY") + builder.markdown( + f"User preference **{user_pref:.6f}** is compliant. " + f"It exceeds the exchange minimum by {((user_pref / exchange_min) - 1) * 100:.1f}% " + f"and the {config.buffer_pct:.0f}% buffered floor." + ) + + builder.manual_order() + await builder.save() + except Exception as e: + logger.warning(f"Report generation failed: {e}") + + from routines.base import RoutineResult + + return RoutineResult( + text=( + f"Order Size Validation ({config.trading_pair}) — **{status}**: {status_detail}. " + f"Recommended size: {recommended_size:.6f}" + ), + sections=[ + {"type": "kpi", "label": "Status", "value": status}, + {"type": "kpi", "label": "Exchange Min", "value": f"{exchange_min:.6f}"}, + {"type": "kpi", "label": "Recommended", "value": f"{recommended_size:.6f}"}, + {"type": "kpi", "label": "Buffer", "value": f"{config.buffer_pct:.0f}%"}, + ], + ) + + except Exception as e: + logger.error(f"order_size_validator error: {e}") + return f"Error running order size validation: {e}" From e03f6e0bdf30b60757f3398dc5bf972e6a9d4835 Mon Sep 17 00:00:00 2001 From: rapcmia Date: Fri, 31 Jul 2026 00:13:11 +0800 Subject: [PATCH 2/6] feat(agents): add liquidation guard skill and grid strategies Reworks the adaptive grid trader around a two-layer decision system: baseline_7d decides the first grid, hourly_mtf_check (1h/4h/1d) manages the running one behind an anti-flip rule. - add skills/liquidation_guard: pre-deploy gate covering order sizing, worst-case fill position, and liquidation-vs-limit_price check - drop routines/order_size_validator; sizing is now step 0 of the gate - add btc_usdt_adaptive_grid and sol_usdt_adaptive_grid strategies with fixed risk envelopes and per-tick procedures - rewrite both routines for ATR-derived ranges and EMA trend detection Co-Authored-By: Claude Opus 5 (1M context) --- agents/adaptive_grid_trader/AGENT.md | 214 ++++++-- .../routines/baseline_7d.py | 271 +++++----- .../routines/hourly_mtf_check.py | 488 +++++++++++------- .../routines/order_size_validator.py | 169 ------ .../skills/liquidation_guard/SKILL.md | 78 +++ .../btc_usdt_adaptive_grid/strategy.md | 195 +++++++ .../sol_usdt_adaptive_grid/strategy.md | 194 +++++++ 7 files changed, 1080 insertions(+), 529 deletions(-) delete mode 100644 agents/adaptive_grid_trader/routines/order_size_validator.py create mode 100644 agents/adaptive_grid_trader/skills/liquidation_guard/SKILL.md create mode 100644 agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md create mode 100644 agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md diff --git a/agents/adaptive_grid_trader/AGENT.md b/agents/adaptive_grid_trader/AGENT.md index 91fafb37..e6f81593 100644 --- a/agents/adaptive_grid_trader/AGENT.md +++ b/agents/adaptive_grid_trader/AGENT.md @@ -2,7 +2,7 @@ name: Adaptive Grid Trader description: Expert in multi-timeframe adaptive grid trading with safety-first order sizing, 20% reserve requirement, and strict risk management -agent_key: openrouter:anthropic/claude-sonnet-4.5 +agent_key: claude-acp:opus tools: - get_market_data - get_portfolio_overview @@ -11,9 +11,10 @@ tools: - manage_routines - trading_agent_journal_read - trading_agent_journal_write +- manage_memory +- manage_skill when_to_consult: When the user wants to deploy, configure, monitor, or refine an adaptive - grid trading strategy that auto-adjusts direction based on market conditions and - enforces safe order sizing with exchange compliance + grid trading strategy that auto-adjusts direction based on market conditions. server_required: true server_name: '' created_by: 1474408604 @@ -26,11 +27,11 @@ You are an expert in **adaptive grid trading** — deploying directional grids ( ## What you DO -- **Multi-timeframe market analysis**: Analyze 7d baseline, then hourly 1h/6h/12h checks to choose grid direction (LONG_GRID, SHORT_GRID, TWO_SIDED_GRID, or HOLD) -- **Safety-first order sizing**: Enforce 20% wallet reserve, compare user preference vs. exchange minimum, suggest buffered order size with user approval -- **Grid construction**: Calculate how many valid orders fit within budget, ensure each order ≥ max(user_preference, exchange_minimum) -- **Risk management**: Configure leverage (3x-10x range), hard stop-loss, trailing stop, and emergency shutdown protocol -- **Position verification**: Cancel all orders, close position with reduce-only, verify position=0, retry with alerts if anything remains +- **Multi-timeframe market analysis**: 7d baseline for initial direction, then hourly 1h/6h/12h checks to manage the running grid +- **Order sizing**: hold back the reserve set in the envelope, and size every order to at least `max(min_order_size, exchange_minimum)` +- **Grid construction**: use the allocated budget to work out how many valid orders fit. LONG or SHORT may use the full allocation; TWO_SIDED splits it 50/50 between the two legs. Build the result as a `grid_executor` payload. +- **Risk management**: set leverage from market risk and account size (1x spot, 3x–10x perps). Set `limit_price` as the grid invalidation price. `keep_position` is always `False`. Set `triple_barrier_config.take_profit` for the per-level profit target. +- **Position verification**: cancel all orders, close the position with reduce-only, verify position = 0, retry within limits, alert if anything remains ## What you do NOT handle @@ -38,56 +39,181 @@ You are an expert in **adaptive grid trading** — deploying directional grids ( - Manual order placement outside grid framework - Backtesting (defer to controller configs and backtest tools) +## Setup: what the user gives you once + +The user approves these **once**, at setup. After that you run on your own and **never ask permission per trade**. + +- `pair` — market to trade +- `budget` — total quote currency the strategy may use +- `reserve_pct` — held back, never traded (default 10%) +- `max_leverage` — hard ceiling +- `max_loss_pct` — **the most important one.** The largest acceptable loss for a single grid, as a % of budget. Any grid whose loss at `limit_price` would exceed this is not allowed to deploy. +- `min_order_size` — the user's preferred floor per order +- `allowed_profiles` — which of LONG / SHORT / TWO_SIDED you may use + +If any of these is missing, ask once at setup. Then stop asking. + +## How autonomy works + +- **Inside the envelope → act.** Deploy, stop, or replace without asking. +- **Outside the envelope → decline and report.** Do not ask for permission and do not block the loop. Skip the trade, say why, wait for the next checkpoint. +- **Broken or unsafe state → stop trading and alert.** This is the only case that halts the loop. Triggers: a leftover position you cannot verify as closed, retries exhausted, or liquidation price sitting inside `limit_price`. + ## Core Logic ### Pre-Trade Safety Checks 1. Read wallet balance -2. Require Trading Budget + 20% reserve (e.g., 100 USDT budget → need 120 USDT available) -3. Check exchange minimum order size in background -4. Compare user's preferred minimum vs. exchange minimum -5. Suggest buffered order size (e.g., if both are 5 USDT, suggest 6 USDT) -6. Get user approval before using larger size (unless auto-approve flag enabled) - -### Market Decision Flow -- **Initial**: Analyze previous 7 days as baseline -- **Hourly**: Analyze 1h, 6h, 12h data -- **Choose**: LONG_GRID, SHORT_GRID, TWO_SIDED_GRID (only on hedge-mode exchanges), or HOLD -- **Anti-flip rule**: Don't quickly flip long ↔ short unless 6h and 12h confirm, except emergency exits +2. Available balance ≥ `budget` + `reserve_pct` +3. Grid's worst-case loss at `limit_price` ≤ `max_loss_pct` +4. Leverage ≤ `max_leverage`, and liquidation price sits beyond `limit_price` (see **Liquidation Guard** below) +5. Every order ≥ `max(min_order_size, exchange_minimum)` +6. **Any check fails → HOLD and report.** Never raise the budget to make a grid fit. + +### Market Decision Flow — Two-Layer System + +The decision logic has two distinct layers: + +**Layer 1 — Baseline (7d): decides the FIRST grid** +- Run `baseline_7d` at startup and daily thereafter +- The 7d trend determines the initial grid direction: + - BULLISH → deploy LONG grid + - BEARISH → deploy SHORT grid + - NEUTRAL → HOLD (no first grid until trend emerges) +- This is the entry signal. It gets the agent into the market based on the broader trend. +- Once the first grid is deployed, the baseline's job is done — it becomes reference context for the hourly check. + +**Layer 2 — Hourly check (4h + 1d): manages the RUNNING grid** +- Run `hourly_mtf_check` every tick (~1h) +- The hourly check decides what happens to the grid that's already running: + - 4h + 1d confirm same direction → keep running, no change + - 4h + 1d both NEUTRAL → keep running (HOLD ≠ stop — grid continues passively) + - 4h + 1d both confirm OPPOSITE direction → teardown and deploy new grid in the opposite direction + - 4h + 1d disagree → keep running, no change +- The hourly check also uses 1h data for range/volatility context but 1h alone never triggers a direction change. + +**Key rules across both layers:** +- **Anti-flip rule**: a direction change requires **both** 4h and 1d to agree with the new direction. The baseline does not override this for subsequent grids — it only applies to the first entry. +- **Minimum grid lifetime**: no discretionary profile change within 3h of deployment (tunable). Prevents churn when 4h/1d flip shortly after a deploy. +- **Emergency exits are exempt** from both the anti-flip rule and minimum lifetime. Capital preservation overrides patience. An emergency exit gets you flat — it does not authorize the opposite grid, which still needs 4h/1d confirmation. + +**Special case — grid died on its own:** +If the grid stopped itself (`limit_price` hit, `time_limit` expired, or `CloseType.FAILED`), the agent is back to "no grid running." In this case: +- If the hourly check has a directional signal (both 4h + 1d agree) → deploy in that direction +- If hourly check is NEUTRAL/disagreed → fall back to the latest baseline trend for direction +- If baseline is also NEUTRAL → HOLD + +**Profiles**: `LONG_GRID`, `SHORT_GRID`, `TWO_SIDED_GRID` (hedge-mode only), `HOLD` +- `TWO_SIDED_GRID` is **two executors** (`side=BUY` plus `side=SELL`), not one two-sided executor — a `grid_executor` has a single `side` +- `HOLD` and `TWO_SIDED_GRID` are not the same thing: `HOLD` means direction is unreadable or no valid grid can be built; `TWO_SIDED_GRID` means range-bound conditions are *positively confirmed* + +**Read actual state, never the stored profile**: at each checkpoint, query live executor status and exchange positions. A grid may have already closed itself, and TWO_SIDED legs can die independently. + +**Transitions** (shown from LONG; SHORT is symmetric) + +| Hourly signal | Action | +|---|---| +| 4h+1d BULLISH | Leave running. No action. | +| 4h+1d NEUTRAL or disagree | Leave running — HOLD is passive, grid keeps laddering. | +| 4h+1d both BEARISH (confirmed) | Close grid → verify position = 0 → deploy SHORT. | ### Grid Rules -- Calculate how many valid orders fit within Trading Budget -- No order below max(user_preference, exchange_minimum) -- If no safe valid grid can be built → HOLD -- Never start new grid until old position fully closed + +**Range construction** +- **Size the range to the expected grid lifetime, not to the next checkpoint.** Because the anti-flip rule requires 4h/1d confirmation, a grid's realistic lifetime is 6–12h, not 1h. Sizing to one hour guarantees the range is stale at every checkpoint. +- Let `D = ATR(1h) × √(lifetime_hours)`. For LONG: `start_price = price − D`, `end_price = price + 3D`, `limit_price ≤ price − 1.5D`. SHORT mirrors. Keep the asymmetry — entry zone on one side, room to run on the other. +- **`limit_price` must sit outside the normal noise band** so ordinary volatility cannot stop the grid out. It is a thesis-invalidation level, not a random exit. +- **Never use fixed percentages.** Derive boundaries from each interval's market data. (`calculate_auto_prices` hardcodes 2% / 3% regardless of pair or volatility — override it.) + +**Sizing and viability** +- **Spacing must clear fees**: `min_spread_between_orders` and `triple_barrier_config.take_profit` must both exceed round-trip fee cost with margin. A 2 bps take-profit is below round-trip maker cost on most venues and tiers, so every completed cycle loses money. +- `levels = range_width ÷ spacing`, then `per_level = total_amount_quote ÷ levels` +- Require `per_level ≥ max(user_preference, exchange_minimum)` — otherwise reduce level count or widen spacing +- `TWO_SIDED_GRID` splits the allocated budget **50/50 between the two legs**, so each leg must pass this viability test on its own half. Expect fewer levels per leg than a one-sided grid at the same budget — that is the cost of being two-sided. If a leg cannot fit enough valid orders on its half, TWO_SIDED is not deployable: widen spacing, fall back to one-sided, or HOLD. **Never raise the allocation to make a leg fit.** +- **If no safe valid grid can be built → HOLD** + +**Teardown discipline** +- Any change to range or direction is a **full teardown**: cancel all → close position → verify position = 0 → redeploy. There is no in-place adjustment; a bare `grid_executor` never re-ranges itself, so the agent is the control loop. +- `keep_position` is always **False**, so every teardown realizes PnL at market. Treat re-ranging as expensive and do it rarely. +- **Never deploy a new grid until the previous position is verified flat** — verified by querying the exchange, not inferred from the executor having stopped. `CloseType.FAILED` (10 failed close retries, ~1 minute) leaves the executor stopped with inventory still open. ### Risk & Shutdown -- **Leverage**: 3x-5x for dry run, 3x-10x design range -- **Hard stop-loss**: Protect total grid loss -- **Trailing stop**: Protect profit after grid becomes profitable -- **Emergency shutdown protocol**: - 1. Cancel all remaining grid orders - 2. Close remaining position (reduce-only) - 3. Verify position size = 0 - 4. Retry safely and alert if anything remains - 5. Never start another grid until old position fully closed + +**Leverage** +- 1x for spot. Perpetuals: 3x–5x for dry run, 3x–10x design range. +- **Verify the liquidation price sits beyond `limit_price`**, computed at *full grid deployment* — every level filled, price at the adverse end of the range. That is the worst case, not the current state: a grid accumulates inventory as price moves against it, so effective leverage rises over the grid's life. If liquidation would trigger before `limit_price`, the exchange closes you on its terms instead of yours — reduce leverage or narrow the range. + +**Liquidation Guard — pre-deploy computation** + +Run this before every grid deployment. It answers one question: will the exchange liquidate me before `limit_price` fires? + +**Step 1 — Worst-case position size** +Assume every grid level fills. Sum up total position: +- `total_base = Σ (per_level_quote ÷ level_price)` for each level from `start_price` to `end_price` +- `avg_entry = total_amount_quote ÷ total_base` + +**Step 2 — Worst-case liquidation price** +Using isolated-margin formula (most perp exchanges): +- LONG: `liq_price = avg_entry × (1 − 1/leverage + maintenance_margin_rate)` +- SHORT: `liq_price = avg_entry × (1 + 1/leverage − maintenance_margin_rate)` + +Where `maintenance_margin_rate` is the exchange's maintenance margin for the position tier (typically 0.4%–2% depending on size). Use the tier that matches `total_base × limit_price` notional. + +**Step 3 — The check** +- LONG grid: `liq_price` must be **below** `limit_price`. If `liq_price ≥ limit_price` → reject. +- SHORT grid: `liq_price` must be **above** `limit_price`. If `liq_price ≤ limit_price` → reject. + +**Step 4 — If rejected** +Try in order: +1. Reduce leverage by 1 step and recompute +2. If leverage is already at minimum useful level → narrow the range (fewer levels, wider spacing) +3. If still fails → HOLD and report: "liquidation guard blocked deployment — liq_price {X} is inside limit_price {Y} at {Z}x leverage" + +**Why this matters**: `limit_price` only protects you if the exchange hasn't already liquidated you. At full fill with high leverage, liquidation can be closer than you think. This check guarantees your exit fires first. + +**Exit policy — `limit_price` only** +- `limit_price` is the **sole** downside barrier. `triple_barrier_config` carries only `take_profit`, `open_order_type`, and `take_profit_order_type`. **Do not set `stop_loss` or `trailing_stop`** — the executor supports both, but this design deliberately omits them in favour of a single price-based exit. +- Because it is the only protection, `limit_price` must satisfy two competing requirements at once: + - **Far enough out** that ordinary volatility cannot stop the grid out (outside the noise band) + - **Close enough in** that the loss when it fires is acceptable +- Resolve that tension at **design time, not runtime**. A price-based barrier makes worst-case loss computable before deploying: at `limit_price` with every level filled, loss ≈ Σ over levels of `(fill_price − limit_price) × level_size`. Choose the range and `limit_price` so that figure sits within the user's accepted loss for the grid, and state the number when proposing the grid. This is a stronger guarantee than a reactive PnL stop — it is known up front rather than discovered on trigger. +- **Tradeoffs being accepted:** + - *No profit protection.* A grid up 3% can give all of it back down to `limit_price`. The hourly checkpoint is the only mechanism that can bank an unrealized gain, so profit protection carries up to a 1h lag. + - *No cumulative-PnL cap.* Losses across many closed cycles never trigger anything, since the executor only ever measures unrealized PnL on open inventory. If a total-loss ceiling is required, the agent must track cumulative PnL itself and stop the executor. +- All barrier closes are placed as `OrderType.MARKET` — taker fees plus slippage — and are **not** reduce-only. Reduce-only applies only to manual orphan recovery below. + +**Dead-man's switch** +- Set **`triple_barrier_config.time_limit`**. A `grid_executor` never self-expires and never re-ranges itself, so if the hourly loop stalls or the session ends, the grid keeps running indefinitely on boundaries computed hours ago with nothing supervising it. Size `time_limit` to a small multiple of the expected grid lifetime so an unsupervised grid closes itself. + +**Normal stop** (`keep_position=False`) +- The executor cancels its own orders and closes its own position. Do not duplicate that work. +- **Still verify**: query the exchange for actual position size. Never infer flat from "the executor stopped." + +**Orphan recovery** — run when verification shows a position still open +- Trigger cases: `CloseType.FAILED` (10 failed close retries, ~1 minute) stops the executor with inventory still open; also partial fills and rejected close orders. +1. Cancel any remaining orders on the pair +2. Close the remaining position with a **reduce-only** order — reduce-only can only shrink or flatten a position, so a wrong size can never flip you into the opposite side +3. Re-query the exchange and confirm position size = 0 +4. Bounded retries only, then **stop and alert a human**. Never retry indefinitely and never fail silently. +5. **Never deploy another grid while any position remains unverified** ## How you answer -- **Lead with the recommendation** (action, direction, order size, leverage) -- **Key: value format**, not prose -- **Show your work**: what timeframes say, what the safety check found, why HOLD vs. trade -- **Before any trade**: confirm user has approved the specific parameters (pair, budget, order size, leverage) -- **On errors**: read the failure, explain in plain terms, propose fix +You **report what you did**. You are not asking for permission. + +- **Action first**: `no change` | `deploy` | `stop` | `replace` | `blocked` +- **`no change` is a good answer.** Most checkpoints end there. Don't invent activity to look useful. +- **Use `key: value` lines, not paragraphs.** +- **On `deploy`**, list: `pair`, `direction`, `leverage`, `start_price`, `end_price`, `limit_price`, `levels`, `size_per_level`, `worst_case_loss` (in quote currency and as % of budget), `liq_price` (computed), `liq_guard: PASS` +- **On `replace`**, also state the cost — closing realizes PnL at market with taker fees and slippage +- **On `blocked`**, name the check that failed, with its number next to the limit it broke. If liquidation guard failed, include: `liq_price`, `limit_price`, `leverage`, and which step-4 remediation was attempted. +- **Always include**: what 1h said about the range, what 6h/12h said about direction, and the position size read back from the exchange. Never guess "flat". +- **On errors**: plain words — what failed, what it means, what you did. Never retry silently. If retries run out, stop and alert. ## Memory & Skills You own domain memory (market learnings, user preferences for this strategy) and reusable skills (e.g., "how to size orders with buffer", "emergency shutdown checklist"). Use `manage_memory` and `manage_skill` to refine your judgment over time. -## Routines (to be added) - -You will call analysis routines by name: -- `baseline_7d`: Initial 7-day market analysis -- `hourly_mtf_check`: 1h/6h/12h multi-timeframe analysis -- `order_size_validator`: Check exchange minimums and suggest buffer +## Routines -When running on a loop, call these routines, read their output, and decide. +- `baseline_7d` — computes 7d ATR, range, trend direction/strength. Run at startup and daily. +- `hourly_mtf_check` — multi-timeframe analysis (1h/4h/1d) → grid profile recommendation with price levels. Run every tick. diff --git a/agents/adaptive_grid_trader/routines/baseline_7d.py b/agents/adaptive_grid_trader/routines/baseline_7d.py index c72c2515..9862afb0 100644 --- a/agents/adaptive_grid_trader/routines/baseline_7d.py +++ b/agents/adaptive_grid_trader/routines/baseline_7d.py @@ -2,7 +2,6 @@ from telegram.ext import ContextTypes from config_manager import get_client import logging -import statistics logger = logging.getLogger(__name__) @@ -10,11 +9,36 @@ class Config(BaseModel): - """7-day baseline analysis: trend direction, support/resistance levels, and ATR volatility for the adaptive grid trader.""" - - connector_name: str = Field(default="binance_perpetual", description="Exchange connector name") - trading_pair: str = Field(default="BTC-USDT", description="Trading pair to analyze") - lookback_days: int = Field(default=7, description="Number of days to look back (uses 4h candles)") + """Compute 7-day baseline stats: ATR, range, trend for a perpetual pair.""" + + trading_pair: str = Field(default="BTC-USDT") + connector_name: str = Field(default="binance_perpetual") + atr_period: int = Field(default=14, description="Candles for ATR calculation") + + +def _ema(values: list, period: int) -> list: + if len(values) < period: + return [] + k = 2.0 / (period + 1) + seed = sum(values[:period]) / period + result = [seed] + for v in values[period:]: + result.append(v * k + result[-1] * (1 - k)) + return result + + +def _compute_atr(candles: list, period: int) -> float: + if len(candles) < period + 1: + return 0.0 + true_ranges = [] + for i in range(1, len(candles)): + h = float(candles[i]["high"]) + lo = float(candles[i]["low"]) + prev_c = float(candles[i - 1]["close"]) + tr = max(h - lo, abs(h - prev_c), abs(lo - prev_c)) + true_ranges.append(tr) + recent_trs = true_ranges[-period:] + return sum(recent_trs) / len(recent_trs) async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: @@ -22,139 +46,122 @@ async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: if not client: return "No server available" + # --- Fetch 7 days of 1h candles --- try: - max_records = config.lookback_days * 6 + 10 # 6 four-hour candles per day result = await client.market_data.get_candles( - config.connector_name, config.trading_pair, interval="4h", max_records=max_records + config.connector_name, + config.trading_pair, + interval="1h", + max_records=168, ) - records = result if isinstance(result, list) else result.get("data", result.get("candles", [])) - - if not records or len(records) < 5: - return f"Insufficient candle data for {config.trading_pair} on {config.connector_name}" + records = ( + result + if isinstance(result, list) + else result.get("data", result.get("candles", [])) + ) + except Exception as e: + return f"Failed to fetch candles: {e}" - closes = [float(c["close"]) for c in records] - highs = [float(c["high"]) for c in records] - lows = [float(c["low"]) for c in records] - current_price = closes[-1] + if not records or len(records) < 20: + return ( + f"Insufficient candle data — got {len(records) if records else 0} candles, " + "need at least 20." + ) - # Trend: SMA short vs SMA long - sma_short = statistics.mean(closes[-7:]) - sma_long = statistics.mean(closes[-21:]) if len(closes) >= 21 else statistics.mean(closes) + try: + records = sorted(records, key=lambda c: c["timestamp"]) + except Exception: + pass + + highs = [float(c["high"]) for c in records] + lows = [float(c["low"]) for c in records] + closes = [float(c["close"]) for c in records] + + # --- 7D range --- + seven_d_high = max(highs) + seven_d_low = min(lows) + range_pct = (seven_d_high - seven_d_low) / seven_d_low * 100 + + # --- ATR --- + atr = _compute_atr(records, config.atr_period) + current_price = closes[-1] + atr_pct = (atr / current_price * 100) if current_price else 0.0 + + # --- EMAs --- + ema20_series = _ema(closes, 20) + ema50_series = _ema(closes, 50) + + if not ema20_series or not ema50_series: + trend_direction = "NEUTRAL" + trend_strength = "weak" + ema20_val = ema50_val = 0.0 + sep_vs_atr = 0.0 + else: + ema20_val = ema20_series[-1] + ema50_val = ema50_series[-1] + separation = ema20_val - ema50_val + sep_vs_atr = abs(separation) / atr if atr > 0 else 0.0 + + if ema20_val > ema50_val: + rising = len(ema20_series) >= 3 and ema20_series[-1] > ema20_series[-3] + trend_direction = "BULLISH" if rising else "NEUTRAL" + elif ema20_val < ema50_val: + falling = len(ema20_series) >= 3 and ema20_series[-1] < ema20_series[-3] + trend_direction = "BEARISH" if falling else "NEUTRAL" + else: + trend_direction = "NEUTRAL" - if sma_short > sma_long * 1.001: - trend_direction = "up" - elif sma_short < sma_long * 0.999: - trend_direction = "down" + if sep_vs_atr < 0.5: + trend_strength = "weak" + elif sep_vs_atr < 1.5: + trend_strength = "moderate" else: - trend_direction = "range" - - # ATR-based volatility - true_ranges = [] - for i in range(1, len(records)): - high = float(records[i]["high"]) - low = float(records[i]["low"]) - prev_close = float(records[i - 1]["close"]) - tr = max(high - low, abs(high - prev_close), abs(low - prev_close)) - true_ranges.append(tr) - - atr_period = min(14, len(true_ranges)) - atr = statistics.mean(true_ranges[-atr_period:]) - volatility_pct = (atr / current_price) * 100 - - # Support/resistance via swing highs/lows - window = 3 - resistance_levels = [] - support_levels = [] - for i in range(window, len(highs) - window): - if highs[i] == max(highs[i - window : i + window + 1]): - resistance_levels.append(highs[i]) - if lows[i] == min(lows[i - window : i + window + 1]): - support_levels.append(lows[i]) - - resistance_levels.sort(key=lambda x: abs(x - current_price)) - support_levels.sort(key=lambda x: abs(x - current_price)) - - key_levels = [] - for level in resistance_levels[:3]: - key_levels.append( - { - "level": round(level, 4), - "type": "resistance", - "distance_pct": round((level - current_price) / current_price * 100, 2), - } - ) - for level in support_levels[:3]: - key_levels.append( - { - "level": round(level, 4), - "type": "support", - "distance_pct": round((level - current_price) / current_price * 100, 2), - } - ) - key_levels.sort(key=lambda x: abs(x["distance_pct"])) - - period_high = max(highs) - period_low = min(lows) - range_pct = (period_high - period_low) / period_low * 100 - - # Report - try: - from condor.reports import ReportBuilder - - builder = ReportBuilder(f"{config.lookback_days}d Baseline: {config.trading_pair}") - builder.source("routine", "baseline_7d").tags(["analysis", "grid", "baseline"]) - - builder.section("01 / TREND", f"{config.lookback_days}d of 4h candles — SMA cross signal") - builder.kpi("Trend Direction", trend_direction.upper()) - builder.kpi("Current Price", f"{current_price:,.4f}") - builder.kpi("SMA Short (7c)", f"{sma_short:,.4f}") - builder.kpi("SMA Long (21c)", f"{sma_long:,.4f}") - - builder.section("02 / VOLATILITY", "ATR (14-period) on 4h candles") - builder.kpi("ATR (14-period)", f"{atr:,.4f}") - builder.kpi("Volatility %", f"{volatility_pct:.2f}%") - builder.kpi(f"{config.lookback_days}d High", f"{period_high:,.4f}") - builder.kpi(f"{config.lookback_days}d Low", f"{period_low:,.4f}") - builder.kpi("Price Range %", f"{range_pct:.2f}%") - - builder.section("03 / KEY LEVELS", "Nearest swing-high resistances and swing-low supports") - if key_levels: - builder.table( - [ - { - "Level": f"{level['level']:,.4f}", - "Type": level["type"].capitalize(), - "Distance %": f"{level['distance_pct']:+.2f}%", - } - for level in key_levels - ], - ["Level", "Type", "Distance %"], - ) - else: - builder.markdown("_No significant swing levels detected in this window._") - - builder.manual_order() - await builder.save() - except Exception as e: - logger.warning(f"Report generation failed: {e}") - - from routines.base import RoutineResult - - return RoutineResult( - text=( - f"{config.trading_pair} {config.lookback_days}d Baseline | " - f"Trend: **{trend_direction.upper()}** | " - f"Volatility: {volatility_pct:.2f}% | " - f"Key levels: {len(key_levels)}" - ), - sections=[ - {"type": "kpi", "label": "Trend Direction", "value": trend_direction.upper()}, - {"type": "kpi", "label": "Volatility %", "value": f"{volatility_pct:.2f}%"}, - {"type": "kpi", "label": "ATR", "value": f"{atr:,.4f}"}, - {"type": "kpi", "label": "Key Levels", "value": str(len(key_levels))}, - ], + trend_strength = "strong" + + # --- Report --- + try: + from condor.reports import ReportBuilder + + builder = ReportBuilder(f"7D Baseline — {config.trading_pair}") + builder.source("routine", "baseline_7d").tags( + ["baseline", "adaptive_grid", "atr", "trend"] ) + builder.section( + "7-Day Market Snapshot", + f"{config.trading_pair} on {config.connector_name} | {len(records)} × 1h candles", + ) + builder.kpi("Current Price", f"${current_price:,.2f}") + builder.kpi("7D High", f"${seven_d_high:,.2f}") + builder.kpi("7D Low", f"${seven_d_low:,.2f}") + builder.kpi("7D Range %", f"{range_pct:.2f}%") + + builder.section( + "Volatility — ATR", + f"Average True Range over last {config.atr_period} × 1h candles", + ) + builder.kpi("ATR (1h)", f"${atr:,.2f}") + builder.kpi("ATR % of Price", f"{atr_pct:.3f}%") + + builder.section("Trend", "EMA20 vs EMA50 on 1h closes") + builder.kpi("EMA20", f"${ema20_val:,.2f}") + builder.kpi("EMA50", f"${ema50_val:,.2f}") + builder.kpi("EMA Sep / ATR", f"{sep_vs_atr:.2f}×") + builder.kpi("Trend Direction", trend_direction) + builder.kpi("Trend Strength", trend_strength) + + builder.manual_order() + report_id = await builder.save() except Exception as e: - logger.error(f"baseline_7d error: {e}") - return f"Error running baseline analysis: {e}" + logger.warning(f"Report generation failed: {e}") + report_id = None + + summary = ( + f"7D Baseline — {config.trading_pair}\n" + f"Price: ${current_price:,.2f} | High: ${seven_d_high:,.2f} | Low: ${seven_d_low:,.2f}\n" + f"Range: {range_pct:.2f}% | ATR(1h,{config.atr_period}): ${atr:,.2f} ({atr_pct:.3f}%)\n" + f"Trend: {trend_direction} / {trend_strength} | EMA20: ${ema20_val:,.2f} | EMA50: ${ema50_val:,.2f}" + ) + if report_id: + summary += f"\nReport: {report_id}" + return summary diff --git a/agents/adaptive_grid_trader/routines/hourly_mtf_check.py b/agents/adaptive_grid_trader/routines/hourly_mtf_check.py index 96a8ee18..1551fda5 100644 --- a/agents/adaptive_grid_trader/routines/hourly_mtf_check.py +++ b/agents/adaptive_grid_trader/routines/hourly_mtf_check.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field from telegram.ext import ContextTypes from config_manager import get_client -import logging -import statistics import asyncio +import logging +import math logger = logging.getLogger(__name__) @@ -11,201 +11,321 @@ class Config(BaseModel): - """Multi-timeframe grid direction check (1h/6h/12h). Recommends LONG_GRID/SHORT_GRID/TWO_SIDED_GRID/HOLD with confidence 1-5.""" - - connector_name: str = Field(default="binance_perpetual", description="Exchange connector name") - trading_pair: str = Field(default="BTC-USDT", description="Trading pair to analyze") - short_tf: str = Field(default="1h", description="Short timeframe interval") - mid_tf: str = Field(default="6h", description="Mid timeframe interval") - long_tf: str = Field(default="12h", description="Long timeframe interval") - rsi_period: int = Field(default=14, description="RSI calculation period") - - -def _momentum_roc(closes: list, period: int = 14) -> float: - """Rate of change momentum: (current - n_ago) / n_ago * 100.""" - if len(closes) < period + 1: + """Multi-timeframe analysis → LONG_GRID / SHORT_GRID / TWO_SIDED_GRID / HOLD recommendation.""" + trading_pair: str = Field(default="BTC-USDT") + connector_name: str = Field(default="binance_perpetual") + lifetime_hours: float = Field(default=8.0, description="Expected grid lifetime for range calculation (6-12h typical due to anti-flip rule)") + atr_period: int = Field(default=14) + baseline_atr: float = Field(default=0.0, description="From baseline_7d — 0 means compute from 1h candles only") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _parse_candles(result) -> list: + """Defensively parse candles from API response.""" + if result is None: + return [] + if isinstance(result, list): + return result + return result.get("data", result.get("candles", [])) + + +def _compute_ema(closes: list, period: int) -> list: + """Compute EMA series for a list of close prices.""" + if len(closes) < period: + return [] + k = 2.0 / (period + 1) + ema = [sum(closes[:period]) / period] + for price in closes[period:]: + ema.append(price * k + ema[-1] * (1 - k)) + return ema + + +def _compute_atr(candles: list, period: int) -> float: + """Compute ATR(period) from candle dicts.""" + if len(candles) < 2: return 0.0 - return (closes[-1] - closes[-period]) / closes[-period] * 100 - - -def _rsi(closes: list, period: int = 14) -> float: - """Simple RSI.""" - if len(closes) < period + 1: - return 50.0 - gains, losses = [], [] - for i in range(1, len(closes)): - diff = closes[i] - closes[i - 1] - gains.append(max(diff, 0.0)) - losses.append(max(-diff, 0.0)) - avg_gain = statistics.mean(gains[-period:]) - avg_loss = statistics.mean(losses[-period:]) - if avg_loss == 0: - return 100.0 - return 100 - (100 / (1 + avg_gain / avg_loss)) - - -async def _fetch(client, connector: str, pair: str, interval: str, records: int) -> list: - result = await client.market_data.get_candles(connector, pair, interval=interval, max_records=records) - return result if isinstance(result, list) else result.get("data", result.get("candles", [])) - + trs = [] + for i in range(1, len(candles)): + high = float(candles[i].get("high", 0) or 0) + low = float(candles[i].get("low", 0) or 0) + prev_close = float(candles[i - 1].get("close", 0) or 0) + tr = max(high - low, abs(high - prev_close), abs(low - prev_close)) + trs.append(tr) + if not trs: + return 0.0 + if len(trs) < period: + return sum(trs) / len(trs) + # Wilder's smoothed ATR + atr = sum(trs[:period]) / period + for tr in trs[period:]: + atr = (atr * (period - 1) + tr) / period + return atr + + +def _trend_direction(candles: list, fast: int = 9, slow: int = 21) -> str: + """Determine trend via EMA crossover (fast / slow). Returns BULLISH / BEARISH / NEUTRAL.""" + if len(candles) < slow + 2: + return "NEUTRAL" + closes = [float(c.get("close", 0) or 0) for c in candles] + fast_ema = _compute_ema(closes, fast) + slow_ema = _compute_ema(closes, slow) + if not fast_ema or not slow_ema: + return "NEUTRAL" + diff_pct = (fast_ema[-1] - slow_ema[-1]) / slow_ema[-1] * 100 if slow_ema[-1] else 0 + if diff_pct > 0.3: + return "BULLISH" + elif diff_pct < -0.3: + return "BEARISH" + return "NEUTRAL" + + +def _volatility_level(atr: float, candles_24: list) -> tuple: + """Return (vol_level, range_high, range_low, range_pct, range_position) from last 24 1h candles.""" + highs = [float(c.get("high", 0) or 0) for c in candles_24] + lows = [float(c.get("low", 0) or 0) for c in candles_24] + closes = [float(c.get("close", 0) or 0) for c in candles_24] + range_high = max(highs) if highs else 0.0 + range_low = min(lows) if lows else 0.0 + current = closes[-1] if closes else 0.0 + range_size = range_high - range_low + range_pct = (range_size / current * 100) if current else 0.0 + range_pos = (current - range_low) / range_size if range_size > 0 else 0.5 + avg_candle_range = range_size / len(candles_24) if candles_24 else 1.0 + vol_ratio = atr / avg_candle_range if avg_candle_range else 1.0 + if vol_ratio > 1.5: + vol_level = "HIGH" + elif vol_ratio > 0.8: + vol_level = "MODERATE" + else: + vol_level = "LOW" + return vol_level, range_high, range_low, range_pct, range_pos + + +def _confidence(trend_4h: str, trend_1d: str, closes_1h: list) -> str: + """Return HIGH / MEDIUM / LOW based on 3-TF agreement.""" + if trend_4h == "NEUTRAL" and trend_1d == "NEUTRAL": + return "MEDIUM" + if trend_4h != trend_1d: + return "LOW" + # 4h and 1d agree on a direction — check 1h alignment + fast = _compute_ema(closes_1h, 9) + slow = _compute_ema(closes_1h, 21) + if fast and slow and slow[-1]: + diff = (fast[-1] - slow[-1]) / slow[-1] * 100 + aligned = (trend_4h == "BULLISH" and diff > 0.3) or (trend_4h == "BEARISH" and diff < -0.3) + return "HIGH" if aligned else "MEDIUM" + return "MEDIUM" + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: client = await get_client(context._chat_id, context=context) if not client: return "No server available" + # -- 1. Fetch three timeframes in parallel -- try: - raw_short, raw_mid, raw_long = await asyncio.gather( - _fetch(client, config.connector_name, config.trading_pair, config.short_tf, 50), - _fetch(client, config.connector_name, config.trading_pair, config.mid_tf, 30), - _fetch(client, config.connector_name, config.trading_pair, config.long_tf, 20), + raw_1h, raw_4h, raw_1d = await asyncio.gather( + client.market_data.get_candles(config.connector_name, config.trading_pair, "1h", max_records=50), + client.market_data.get_candles(config.connector_name, config.trading_pair, "4h", max_records=30), + client.market_data.get_candles(config.connector_name, config.trading_pair, "1d", max_records=20), ) - - tf_configs = [ - (config.short_tf, raw_short), - (config.mid_tf, raw_mid), - (config.long_tf, raw_long), + except Exception as e: + logger.error(f"[hourly_mtf_check] candle fetch failed: {e}") + return f"Error fetching market data: {e}" + + candles_1h = _parse_candles(raw_1h) + candles_4h = _parse_candles(raw_4h) + candles_1d = _parse_candles(raw_1d) + + if not candles_1h: + return "No 1h candle data available — cannot proceed." + + # -- 2. Per-timeframe analysis -- + # 1h: ATR, range, volatility + atr_1h = _compute_atr(candles_1h, config.atr_period) + if config.baseline_atr > 0: + atr_1h = (atr_1h + config.baseline_atr) / 2.0 + + window_24 = candles_1h[-24:] if len(candles_1h) >= 24 else candles_1h + vol_level, range_high, range_low, range_pct, range_pos = _volatility_level(atr_1h, window_24) + current_price = float(candles_1h[-1].get("close", 0) or 0) + + # 4h (proxy for 6h): trend direction + trend_4h = _trend_direction(candles_4h) if candles_4h else "NEUTRAL" + + # 1d (proxy for 12h): trend confirmation + trend_1d = _trend_direction(candles_1d) if candles_1d else "NEUTRAL" + + # -- 3. Signal synthesis -- + if trend_4h == "BULLISH" and trend_1d == "BULLISH": + profile = "LONG_GRID" + rationale = ("Both medium (4h) and macro (1d) timeframes confirm bullish momentum. " + "Grid skewed upward — capture breakout while limiting downside exposure.") + elif trend_4h == "BEARISH" and trend_1d == "BEARISH": + profile = "SHORT_GRID" + rationale = ("Both medium (4h) and macro (1d) timeframes confirm bearish pressure. " + "Grid skewed downward — profit from continued decline with capped upside risk.") + elif vol_level in ("LOW", "MODERATE"): + profile = "TWO_SIDED_GRID" + rationale = ("Timeframes disagree or both neutral with contained volatility — " + "price is likely range-bound. Symmetric two-sided grid maximises fee capture.") + else: + profile = "HOLD" + rationale = ("Conflicting signals combined with elevated volatility — " + "directional conviction is insufficient. Avoid new grid entry.") + + closes_1h = [float(c.get("close", 0) or 0) for c in candles_1h] + conf = _confidence(trend_4h, trend_1d, closes_1h) + + # -- 4. Price level computation: D = ATR(1h) × √(lifetime_hours) -- + D = atr_1h * math.sqrt(config.lifetime_hours) + + if profile == "LONG_GRID": + start_price = current_price - D + end_price = current_price + 3.0 * D + limit_price = current_price - 1.5 * D + elif profile == "SHORT_GRID": + start_price = current_price - 3.0 * D + end_price = current_price + D + limit_price = current_price + 1.5 * D + elif profile == "TWO_SIDED_GRID": + start_price = current_price - 2.0 * D + end_price = current_price + 2.0 * D + limit_price = None + else: # HOLD — reference only + start_price = current_price - D + end_price = current_price + D + limit_price = None + + # -- Build levels table -- + if profile == "LONG_GRID": + levels_rows = [ + {"Level": "limit_price", "Price": f"{limit_price:,.4f}", "Description": "Abort / stop-loss if breached"}, + {"Level": "start_price", "Price": f"{start_price:,.4f}", "Description": "Grid lower bound"}, + {"Level": "current_price", "Price": f"{current_price:,.4f}", "Description": "Reference at analysis time"}, + {"Level": "end_price", "Price": f"{end_price:,.4f}", "Description": "Grid upper bound / TP zone"}, + ] + elif profile == "SHORT_GRID": + levels_rows = [ + {"Level": "end_price", "Price": f"{end_price:,.4f}", "Description": "Grid upper bound"}, + {"Level": "current_price", "Price": f"{current_price:,.4f}", "Description": "Reference at analysis time"}, + {"Level": "start_price", "Price": f"{start_price:,.4f}", "Description": "Grid lower bound / TP zone"}, + {"Level": "limit_price", "Price": f"{limit_price:,.4f}", "Description": "Abort / stop-loss if breached"}, + ] + elif profile == "TWO_SIDED_GRID": + levels_rows = [ + {"Level": "start_price", "Price": f"{start_price:,.4f}", "Description": "Grid lower bound (−2D)"}, + {"Level": "current_price", "Price": f"{current_price:,.4f}", "Description": "Reference / centre"}, + {"Level": "end_price", "Price": f"{end_price:,.4f}", "Description": "Grid upper bound (+2D)"}, + ] + else: + levels_rows = [ + {"Level": "ref_low", "Price": f"{start_price:,.4f}", "Description": "Reference lower (−D) — NOT ACTIONABLE"}, + {"Level": "current_price", "Price": f"{current_price:,.4f}", "Description": "Reference at analysis time"}, + {"Level": "ref_high", "Price": f"{end_price:,.4f}", "Description": "Reference upper (+D) — NOT ACTIONABLE"}, ] - tf_data = {} - for tf_name, raw in tf_configs: - if not raw or len(raw) < 5: - continue - closes = [float(c["close"]) for c in raw] - sma_short = statistics.mean(closes[-5:]) - sma_long = statistics.mean(closes) - mom = _momentum_roc(closes, period=min(config.rsi_period, len(closes) - 1)) - rsi = _rsi(closes, period=config.rsi_period) - - if sma_short > sma_long * 1.001: - trend = "bullish" - elif sma_short < sma_long * 0.999: - trend = "bearish" - else: - trend = "neutral" - - tf_data[tf_name] = { - "closes": closes, - "momentum": mom, - "rsi": rsi, - "current": closes[-1], - "trend": trend, - "sma_short": sma_short, - "sma_long": sma_long, - } - - if not tf_data: - return "Could not fetch candle data for any timeframe" - - # Alignment scoring - bullish_count = sum(1 for d in tf_data.values() if d["trend"] == "bullish") - bearish_count = sum(1 for d in tf_data.values() if d["trend"] == "bearish") - total = len(tf_data) - - # Divergence: momentum sign differs across timeframes - mom_signs = [1 if d["momentum"] >= 0 else -1 for d in tf_data.values()] - has_divergence = len(set(mom_signs)) > 1 - - # RSI overbought/oversold flags - rsi_flags = [] - for tf_name, d in tf_data.items(): - if d["rsi"] > 70: - rsi_flags.append(f"{tf_name} RSI overbought ({d['rsi']:.1f})") - elif d["rsi"] < 30: - rsi_flags.append(f"{tf_name} RSI oversold ({d['rsi']:.1f})") - - # Recommendation logic - if bullish_count == total: - recommendation = "LONG_GRID" - confidence = 5 if not has_divergence else 4 - reasoning = f"All {total} timeframes bullish" - elif bearish_count == total: - recommendation = "SHORT_GRID" - confidence = 5 if not has_divergence else 4 - reasoning = f"All {total} timeframes bearish" - elif bullish_count > bearish_count: - recommendation = "LONG_GRID" - confidence = 4 if not has_divergence else 3 - reasoning = f"{bullish_count}/{total} timeframes bullish" - elif bearish_count > bullish_count: - recommendation = "SHORT_GRID" - confidence = 4 if not has_divergence else 3 - reasoning = f"{bearish_count}/{total} timeframes bearish" - elif has_divergence: - recommendation = "HOLD" - confidence = 2 - reasoning = "Mixed signals with momentum divergence — unsafe to commit direction" - else: - recommendation = "TWO_SIDED_GRID" - confidence = 3 - reasoning = "Balanced momentum across timeframes — ranging/neutral conditions" - - if rsi_flags: - reasoning += f"; {', '.join(rsi_flags)}" - - divergence_list = [] - if has_divergence: - divergence_list.append("Momentum direction divergence across timeframes") - divergence_list.extend(rsi_flags) - - tf_table = [ + # -- 5. ReportBuilder -- + try: + from condor.reports import ReportBuilder + + builder = ReportBuilder(f"MTF Check — {config.trading_pair}") + builder.source("routine", "hourly_mtf_check").tags(["grid", "mtf", "analysis", "adaptive"]) + + # Section 1: Timeframe Summary + builder.section("01 / TIMEFRAME SUMMARY", "Per-timeframe: trend direction, ATR/volatility, signal") + tf_rows = [ + { + "Timeframe": "1h (Range & Vol)", + "Candles": len(candles_1h), + "ATR(14)": f"{atr_1h:.4f}", + "24h Range": f"{range_low:,.2f} – {range_high:,.2f}", + "Range %": f"{range_pct:.2f}%", + "Volatility": vol_level, + "Price in Range": f"{range_pos:.1%}", + "Signal": "—", + }, { - "Timeframe": tf_name, - "Trend": d["trend"].capitalize(), - "Momentum": f"{d['momentum']:+.2f}%", - "RSI": f"{d['rsi']:.1f}", - "SMA Short": f"{d['sma_short']:,.4f}", - "SMA Long": f"{d['sma_long']:,.4f}", - } - for tf_name, d in tf_data.items() + "Timeframe": "4h proxy (Trend)", + "Candles": len(candles_4h), + "ATR(14)": "—", + "24h Range": "—", + "Range %": "—", + "Volatility": "—", + "Price in Range": "—", + "Signal": trend_4h, + }, + { + "Timeframe": "1d proxy (Confirm)", + "Candles": len(candles_1d), + "ATR(14)": "—", + "24h Range": "—", + "Range %": "—", + "Volatility": "—", + "Price in Range": "—", + "Signal": trend_1d, + }, ] - - # Report - try: - from condor.reports import ReportBuilder - - builder = ReportBuilder(f"MTF Grid Check: {config.trading_pair}") - builder.source("routine", "hourly_mtf_check").tags(["analysis", "grid", "mtf"]) - - builder.section("01 / RECOMMENDATION") - builder.kpi("Recommendation", recommendation) - builder.kpi("Confidence", f"{confidence} / 5") - builder.kpi("Bullish TFs", f"{bullish_count}/{total}") - builder.kpi("Bearish TFs", f"{bearish_count}/{total}") - - builder.section( - "02 / TIMEFRAME BREAKDOWN", - f"Timeframes analyzed: {config.short_tf}, {config.mid_tf}, {config.long_tf}", - ) - builder.table(tf_table, ["Timeframe", "Trend", "Momentum", "RSI", "SMA Short", "SMA Long"]) - - if divergence_list: - builder.section("03 / DIVERGENCES & FLAGS") - builder.markdown("\n".join(f"- {d}" for d in divergence_list)) - - builder.section("04 / REASONING") - builder.markdown(reasoning) - - builder.manual_order() - await builder.save() - except Exception as e: - logger.warning(f"Report generation failed: {e}") - - from routines.base import RoutineResult - - return RoutineResult( - text=( - f"{config.trading_pair} MTF Check → **{recommendation}** " - f"(confidence {confidence}/5) | {reasoning}" - ), - sections=[ - {"type": "kpi", "label": "Recommendation", "value": recommendation}, - {"type": "kpi", "label": "Confidence", "value": f"{confidence}/5"}, - {"type": "kpi", "label": "Bullish TFs", "value": f"{bullish_count}/{total}"}, - {"type": "kpi", "label": "Bearish TFs", "value": f"{bearish_count}/{total}"}, - ], + builder.table(tf_rows, ["Timeframe", "Candles", "ATR(14)", "24h Range", "Range %", "Volatility", "Price in Range", "Signal"]) + + # Section 2: Signal Synthesis + builder.section("02 / SIGNAL SYNTHESIS", "Agreement analysis across timeframes and final profile decision") + builder.kpi("4h Trend", trend_4h) + builder.kpi("1d Trend", trend_1d) + builder.kpi("1h Volatility", vol_level) + builder.kpi("Confidence", conf) + agreement_str = "AGREE ✓" if trend_4h == trend_1d else "DISAGREE ✗" + builder.markdown( + f"**4h vs 1d agreement:** {agreement_str} \n" + f"**Current price:** {current_price:,.4f} \n" + f"**ATR(1h, {config.atr_period}):** {atr_1h:.6f}" + + (f" *(blended with baseline {config.baseline_atr})*" if config.baseline_atr > 0 else "") + + f" \n**D = ATR × √{config.lifetime_hours:.1f}h = {D:.6f}**" ) + # Section 3: Recommendation + builder.section("03 / RECOMMENDATION", "Actionable grid profile with concrete price levels") + builder.kpi("Profile", profile) + builder.kpi("Confidence", conf) + builder.kpi("D Value", f"{D:.4f}") + builder.kpi("Current Price", f"{current_price:,.4f}") + + actionable = profile != "HOLD" + if limit_price is not None: + builder.kpi("Limit Price", f"{limit_price:,.4f}") + builder.kpi("Start Price", f"{start_price:,.4f}" + ("" if actionable else " (ref)")) + builder.kpi("End Price", f"{end_price:,.4f}" + ("" if actionable else " (ref)")) + + builder.table(levels_rows, ["Level", "Price", "Description"]) + builder.markdown(f"**Rationale:** {rationale}") + + builder.manual_order() + report_id = await builder.save() + logger.info(f"[hourly_mtf_check] report saved: {report_id}") except Exception as e: - logger.error(f"hourly_mtf_check error: {e}") - return f"Error running MTF check: {e}" + logger.warning(f"[hourly_mtf_check] report generation failed: {e}") + + # -- Return structured text for agent parsing -- + lines = [ + f"MTF Check — {config.trading_pair}", + f"PROFILE: {profile} | CONFIDENCE: {conf}", + f"4h: {trend_4h} | 1d: {trend_1d} | Volatility: {vol_level}", + f"Current price: {current_price:,.4f} | ATR(1h): {atr_1h:.6f} | D: {D:.6f}", + ] + if profile == "LONG_GRID": + lines.append(f"limit_price={limit_price:.4f} | start_price={start_price:.4f} | end_price={end_price:.4f}") + elif profile == "SHORT_GRID": + lines.append(f"start_price={start_price:.4f} | end_price={end_price:.4f} | limit_price={limit_price:.4f}") + elif profile == "TWO_SIDED_GRID": + lines.append(f"start_price={start_price:.4f} | end_price={end_price:.4f}") + else: + lines.append(f"ref_low={start_price:.4f} | ref_high={end_price:.4f} [NOT ACTIONABLE]") + lines.append(f"Rationale: {rationale}") + return "\n".join(lines) diff --git a/agents/adaptive_grid_trader/routines/order_size_validator.py b/agents/adaptive_grid_trader/routines/order_size_validator.py deleted file mode 100644 index 76f82c60..00000000 --- a/agents/adaptive_grid_trader/routines/order_size_validator.py +++ /dev/null @@ -1,169 +0,0 @@ -from pydantic import BaseModel, Field -from telegram.ext import ContextTypes -from config_manager import get_client -import logging - -logger = logging.getLogger(__name__) - -CATEGORY = "Monitoring" - - -class Config(BaseModel): - """Validates order size against exchange minimums and recommends a buffered safe size. Checks compliance and suggests 10-20% buffer above exchange minimum.""" - - connector_name: str = Field(default="binance_perpetual", description="Exchange connector name") - trading_pair: str = Field(default="BTC-USDT", description="Trading pair to validate") - exchange_min_qty: float = Field( - default=0.001, description="Exchange minimum order quantity in base asset (check exchange docs)" - ) - user_preferred_qty: float = Field( - default=0.001, description="User's intended minimum order quantity in base asset" - ) - buffer_pct: float = Field( - default=20.0, description="Safety buffer % above exchange minimum (10-20 recommended)" - ) - - -async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: - client = await get_client(context._chat_id, context=context) - if not client: - return "No server available" - - try: - # Fetch current price for notional calculations - current_price = 0.0 - try: - prices = await client.market_data.get_prices( - config.connector_name, trading_pairs=[config.trading_pair] - ) - current_price = float(prices.get(config.trading_pair, 0.0)) - except Exception: - pass - - if current_price <= 0: - try: - ob = await client.market_data.get_order_book( - config.connector_name, config.trading_pair, depth=1 - ) - bid = ob["bids"][0][0] if ob.get("bids") else 0 - ask = ob["asks"][0][0] if ob.get("asks") else 0 - current_price = (float(bid) + float(ask)) / 2 if bid and ask else 0.0 - except Exception: - pass - - exchange_min = config.exchange_min_qty - user_pref = config.user_preferred_qty - buffer = config.buffer_pct / 100.0 - - buffered_min = exchange_min * (1 + buffer) - recommended_size = max(buffered_min, user_pref) - - # Compliance flags - is_compliant = user_pref >= exchange_min - meets_buffer = user_pref >= buffered_min - - # Status - if not is_compliant: - status = "FAIL" - status_detail = f"User preference ({user_pref:.6f}) is BELOW exchange minimum ({exchange_min:.6f})" - elif not meets_buffer: - status = "WARN" - status_detail = f"User preference meets exchange minimum but is below {config.buffer_pct:.0f}% buffer ({buffered_min:.6f})" - else: - status = "OK" - status_detail = f"User preference meets the {config.buffer_pct:.0f}% buffered minimum" - - def notional(qty: float) -> str: - return f"${qty * current_price:,.2f}" if current_price > 0 else "N/A" - - rows = [ - { - "Parameter": "Exchange Minimum", - "Qty": f"{exchange_min:.6f}", - "Notional (USD)": notional(exchange_min), - "Note": "Hard floor — orders below this are rejected", - }, - { - "Parameter": f"Buffered Minimum (+{config.buffer_pct:.0f}%)", - "Qty": f"{buffered_min:.6f}", - "Notional (USD)": notional(buffered_min), - "Note": "Recommended floor for safe operation", - }, - { - "Parameter": "User Preference", - "Qty": f"{user_pref:.6f}", - "Notional (USD)": notional(user_pref), - "Note": "✓ Compliant" if is_compliant else "✗ Below exchange minimum", - }, - { - "Parameter": "Recommended Size", - "Qty": f"{recommended_size:.6f}", - "Notional (USD)": notional(recommended_size), - "Note": f"max(buffered_min, user_pref) with {config.buffer_pct:.0f}% buffer", - }, - ] - - # Report - try: - from condor.reports import ReportBuilder - - builder = ReportBuilder(f"Order Size Validator: {config.trading_pair}") - builder.source("routine", "order_size_validator").tags(["monitoring", "grid", "order-size"]) - - builder.section("01 / VALIDATION RESULT") - builder.kpi("Status", status) - builder.kpi("Current Price", f"${current_price:,.4f}" if current_price > 0 else "N/A") - builder.kpi("Exchange Min Qty", f"{exchange_min:.6f}") - builder.kpi("Buffered Min Qty", f"{buffered_min:.6f}") - builder.kpi("Recommended Qty", f"{recommended_size:.6f}") - builder.kpi("Buffer Applied", f"{config.buffer_pct:.0f}%") - - builder.section("02 / SIZE COMPARISON TABLE") - builder.table(rows, ["Parameter", "Qty", "Notional (USD)", "Note"]) - - if status == "FAIL": - builder.section("03 / ACTION REQUIRED") - builder.markdown( - f"**User preference ({user_pref:.6f}) is below the exchange minimum ({exchange_min:.6f}).**\n\n" - f"Orders at this size will be rejected by {config.connector_name}. " - f"Set your minimum order quantity to at least **{recommended_size:.6f}** " - f"(exchange min + {config.buffer_pct:.0f}% buffer)." - ) - elif status == "WARN": - builder.section("03 / RECOMMENDATION") - builder.markdown( - f"User preference passes the exchange minimum but sits below the {config.buffer_pct:.0f}% safety buffer.\n\n" - f"Consider using **{buffered_min:.6f}** or higher to reduce the risk of " - f"edge-case rejections due to rounding or fee deductions." - ) - else: - builder.section("03 / SUMMARY") - builder.markdown( - f"User preference **{user_pref:.6f}** is compliant. " - f"It exceeds the exchange minimum by {((user_pref / exchange_min) - 1) * 100:.1f}% " - f"and the {config.buffer_pct:.0f}% buffered floor." - ) - - builder.manual_order() - await builder.save() - except Exception as e: - logger.warning(f"Report generation failed: {e}") - - from routines.base import RoutineResult - - return RoutineResult( - text=( - f"Order Size Validation ({config.trading_pair}) — **{status}**: {status_detail}. " - f"Recommended size: {recommended_size:.6f}" - ), - sections=[ - {"type": "kpi", "label": "Status", "value": status}, - {"type": "kpi", "label": "Exchange Min", "value": f"{exchange_min:.6f}"}, - {"type": "kpi", "label": "Recommended", "value": f"{recommended_size:.6f}"}, - {"type": "kpi", "label": "Buffer", "value": f"{config.buffer_pct:.0f}%"}, - ], - ) - - except Exception as e: - logger.error(f"order_size_validator error: {e}") - return f"Error running order size validation: {e}" diff --git a/agents/adaptive_grid_trader/skills/liquidation_guard/SKILL.md b/agents/adaptive_grid_trader/skills/liquidation_guard/SKILL.md new file mode 100644 index 00000000..4a61d9cb --- /dev/null +++ b/agents/adaptive_grid_trader/skills/liquidation_guard/SKILL.md @@ -0,0 +1,78 @@ +--- +name: liquidation_guard +description: 'Pre-deploy checklist: order size validation + liquidation price guard + for grid executors on perpetual futures' +when_to_use: Before every grid_executor deployment on perpetual futures. Run after + hourly_mtf_check produces a recommendation and before calling manage_executors(action='create'). +created: '2026-07-30T14:22:18Z' +source: agent:adaptive_grid_trader +--- + +## Pre-Deploy Gate — run every step, stop on first FAIL + +### Step 0 — Order Size Validation + +Given: budget, range (start_price → end_price), spacing, min_order_size, exchange_minimum + +1. Compute levels: `levels = (end_price - start_price) / (spacing × mid_price)` +2. Compute per-level size: `per_level = total_amount_quote / levels` +3. Check: `per_level ≥ max(min_order_size, exchange_minimum)` +4. If FAIL: + - Reduce levels (widen spacing) until per_level passes + - If no valid configuration exists → **FAIL: budget too small for this range** + +### Step 1 — Worst-Case Position Size + +Assume every grid level fills (worst case for LONG = price at start_price, worst case for SHORT = price at end_price). + +- For each level, compute: `level_base = per_level_quote / level_price` +- `total_base = Σ level_base` across all levels +- `avg_entry = total_amount_quote / total_base` + +### Step 2 — Worst-Case Liquidation Price + +Using isolated-margin formula: + +- **LONG**: `liq_price = avg_entry × (1 - 1/leverage + maintenance_margin_rate)` +- **SHORT**: `liq_price = avg_entry × (1 + 1/leverage - maintenance_margin_rate)` + +Where `maintenance_margin_rate` depends on the exchange's position tier for the notional `total_base × limit_price`. Typical values: +- Tier 1 (small positions): 0.4% (0.004) +- Tier 2 (medium): 0.5% (0.005) +- Tier 3 (large): 1-2% (0.01-0.02) + +When in doubt, use the higher tier — it's conservative. + +### Step 3 — The Check + +- **LONG grid**: `liq_price` must be **below** `limit_price` + - PASS if `liq_price < limit_price` + - FAIL if `liq_price ≥ limit_price` +- **SHORT grid**: `liq_price` must be **above** `limit_price` + - PASS if `liq_price > limit_price` + - FAIL if `liq_price ≤ limit_price` + +### Step 4 — If FAIL, try remediation (in order) + +1. Reduce leverage by 1 step → recompute from Step 2 +2. If leverage is already at minimum useful level → narrow the range (fewer levels, wider spacing) → recompute from Step 1 +3. If still FAIL → **HOLD and report** + +### Reporting + +On **PASS**, include in deploy report: +- `liq_price: ` +- `liq_guard: PASS` +- `margin_buffer: ` + +On **FAIL**, report: +- `liq_price: ` +- `limit_price: ` +- `leverage: ` +- `liq_guard: FAIL — ` +- `remediation_attempted: ` +- `action: HOLD` + +### Why this matters + +`limit_price` only protects you if the exchange hasn't already liquidated you. At full fill with high leverage, liquidation can be closer than you think. This check guarantees your exit fires first — every time, before every deploy, no exceptions. diff --git a/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md b/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md new file mode 100644 index 00000000..e1609e53 --- /dev/null +++ b/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md @@ -0,0 +1,195 @@ +--- +name: BTC-USDT Adaptive Grid +description: Hourly adaptive grid on BTC-USDT bitget_perpetual — multi-timeframe analysis, + ATR-based ranges, liquidation guard, $60 budget. +agent_key: null +skills: [] +default_config: + connector_name: bitget_perpetual + trading_pair: BTC-USDT + frequency_sec: 3600 + total_amount_quote: 60 + execution_mode: loop + risk_limits: + max_position_size_quote: 300 + max_open_executors: 1 +default_trading_context: '' +created_by: 1474408604 +created_at: '2026-07-30T14:37:33.785613+00:00' +--- + +# BTC-USDT Adaptive Grid — Tick Instructions + +You are the Adaptive Grid Trader running a loop on **BTC-USDT** on **bitget_perpetual**. + +## Envelope (fixed — never exceed) + +- **pair**: BTC-USDT +- **connector**: bitget_perpetual +- **budget**: 60 USDT +- **reserve_pct**: 10% (hold back $6, trade with $54) +- **min_order_size**: 7 USDT +- **max_leverage**: 5x +- **max_loss_pct**: 10% of budget ($6) +- **allowed_profiles**: LONG, SHORT (TWO_SIDED disabled — budget too small for 2 legs at $7/order) + +## Two-Layer Decision System + +**Layer 1 — Baseline (7d): decides the FIRST grid** +- The 7d trend is the entry signal: BULLISH → LONG, BEARISH → SHORT, NEUTRAL → HOLD +- Once the first grid is deployed, the baseline becomes reference context only + +**Layer 2 — Hourly check (4h + 1d): manages the RUNNING grid** +- Decides whether to keep, replace, or stop the running grid +- Direction change requires BOTH 4h and 1d to confirm the new direction + +## Each Tick (every ~1 hour) + +### 1. Baseline check +Run `baseline_7d` if no baseline exists yet or last run was >24h ago: +``` +manage_routines(action="run", name="baseline_7d", strategy_id="adaptive_grid_trader.btc_usdt_adaptive_grid", + config={"trading_pair": "BTC-USDT", "connector_name": "bitget_perpetual"}) +``` +Store the ATR value and trend direction. + +### 2. Market analysis +Run `hourly_mtf_check` to get the current profile recommendation: +``` +manage_routines(action="run", name="hourly_mtf_check", strategy_id="adaptive_grid_trader.btc_usdt_adaptive_grid", + config={"trading_pair": "BTC-USDT", "connector_name": "bitget_perpetual", + "lifetime_hours": 8.0, "baseline_atr": }) +``` +Read the recommendation: profile, confidence, start_price, end_price, limit_price, D value. + +### 3. Read current state +Check what's actually running — never trust stored state: +``` +manage_executors(action="search", connector_names=["bitget_perpetual"], + trading_pairs=["BTC-USDT"], executor_types=["grid_executor"], status="RUNNING") +``` +Also check exchange position: +``` +get_portfolio_overview(connector_names=["bitget_perpetual"], + include_perp_positions=True, include_balances=True, + include_lp_positions=False, include_active_orders=False) +``` + +### 4. Decide + +**Case A — No grid running, first entry:** +- Use the BASELINE trend to decide direction: + - Baseline BULLISH → deploy LONG grid (go to step 6) + - Baseline BEARISH → deploy SHORT grid (go to step 6) + - Baseline NEUTRAL → HOLD, wait for trend to emerge +- This is the only time the baseline directly drives a deploy. + +**Case B — No grid running, grid died on its own (limit_price hit, time_limit, or FAILED):** +- First check hourly signal: if both 4h + 1d confirm a direction → deploy in that direction +- If hourly is NEUTRAL/disagree → fall back to latest baseline trend for direction +- If baseline also NEUTRAL → HOLD + +**Case C — Grid IS running, hourly check says same direction:** +- No change. Journal it. + +**Case D — Grid IS running, hourly check says NEUTRAL or timeframes disagree:** +- No change. Grid keeps running passively (HOLD ≠ stop). + +**Case E — Grid IS running, hourly check says OPPOSITE direction (both 4h + 1d confirm):** +- Apply minimum lifetime: has current grid been running ≥3h? If not → no change. +- If ≥3h → proceed to teardown (step 5) then deploy (step 6). + +### 5. Teardown (when needed) +``` +manage_executors(action="stop", executor_id="", keep_position=False) +``` +Then **verify** position is flat: +``` +get_portfolio_overview(connector_names=["bitget_perpetual"], + include_perp_positions=True, include_balances=False, + include_lp_positions=False, include_active_orders=False) +``` +If position ≠ 0 → orphan recovery: +1. Close with reduce-only order +2. Re-check position +3. Max 3 retries, then STOP and alert: `send_notification(text="⚠️ Adaptive Grid: orphan position on BTC-USDT bitget_perpetual, manual intervention needed")` + +**Never deploy a new grid until position is verified flat.** + +### 6. Pre-deploy checks (Liquidation Guard skill) + +Read the `liquidation_guard` skill and follow all steps: + +**Step 0 — Order size**: `per_level = 54 / levels`. Must be ≥ $7. With $54 budget: max 7 levels. + +**Step 1 — Position size**: compute total_base and avg_entry assuming all levels fill. + +**Step 2 — Liquidation price**: +- LONG: `liq_price = avg_entry × (1 - 1/leverage + 0.004)` +- SHORT: `liq_price = avg_entry × (1 + 1/leverage - 0.004)` + +**Step 3 — Check**: LONG: liq_price must be < limit_price. SHORT: liq_price must be > limit_price. + +**Step 4 — If FAIL**: reduce leverage → recompute. If still fails → HOLD and journal why. + +### 7. Deploy + +Build the grid_executor config. ALL fields must be present: + +```python +executor_config = { + "connector_name": "bitget_perpetual", + "trading_pair": "BTC-USDT", + "side": 1, # 1=BUY(LONG), 2=SELL(SHORT) + "start_price": , + "end_price": , + "limit_price": , + "total_amount_quote": 54, # budget minus reserve + "min_order_amount_quote": 7, + "min_spread_between_orders": , + "max_open_orders": 7, # max levels given budget + "activation_bounds": 0.002, # 0.2% — only place orders near price + "order_frequency": 5, + "max_orders_per_batch": 1, + "keep_position": False, # ALWAYS false + "coerce_tp_to_step": True, + "triple_barrier_config": { + "take_profit": , + "open_order_type": 3, # LIMIT_MAKER + "take_profit_order_type": 3, # LIMIT_MAKER + "time_limit": 43200 # 12h dead-man's switch + } +} +``` + +Deploy: +``` +manage_executors(action="create", executor_type="grid_executor", executor_config=, + controller_id="adaptive_grid_trader.btc_usdt_adaptive_grid") +``` + +### 8. Journal +Write every decision: +``` +trading_agent_journal_write(agent_id=, entry_type="action", + text="", reasoning="", tick=) +``` +Write learnings when you discover something new about this market. + +## Key constraints +- **min_spread_between_orders** and **take_profit** must both exceed round-trip fees. Bitget perpetual maker fee is typically 0.02% (2 bps). Round-trip = 4 bps. Set take_profit ≥ 0.001 (10 bps) minimum to have margin. +- **Never set stop_loss or trailing_stop** in triple_barrier_config. +- **TWO_SIDED is disabled** for this strategy — $54 split two ways = $27/leg, only ~3 orders per leg at $7 min. Not viable. +- **Leverage**: start at 3x, max 5x. Always run liquidation guard. +- With ~7 levels max, spacing will be wider than ideal. That's fine — fewer but safer orders. + +## Reporting format +- action: no change | deploy | stop | replace | blocked +- direction: LONG | SHORT +- levels: N at $X each +- range: start → end (limit at X) +- liq_guard: PASS (liq_price: X, buffer: Y%) +- worst_case_loss: $X (Y% of budget) +- 1h: range X–Y, ATR Z, volatility HIGH/MED/LOW +- 4h/1d: BULLISH/BEARISH/NEUTRAL +- baseline: BULLISH/BEARISH/NEUTRAL (7d trend) diff --git a/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md b/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md new file mode 100644 index 00000000..b5d58a96 --- /dev/null +++ b/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md @@ -0,0 +1,194 @@ +--- +name: SOL-USDT Adaptive Grid +description: Hourly adaptive grid on SOL-USDT binance_perpetual — multi-timeframe + analysis, ATR-based ranges, liquidation guard, $100 budget. +agent_key: null +skills: [] +default_config: + connector_name: binance_perpetual + trading_pair: SOL-USDT + frequency_sec: 3600 + total_amount_quote: 100 + execution_mode: loop + risk_limits: + max_position_size_quote: 500 + max_open_executors: 1 +default_trading_context: '' +created_by: 1474408604 +created_at: '2026-07-30T15:47:53.006353+00:00' +--- + +# SOL-USDT Adaptive Grid — Tick Instructions + +You are the Adaptive Grid Trader running a loop on **SOL-USDT** on **binance_perpetual**. + +## Envelope (fixed — never exceed) + +- **pair**: SOL-USDT +- **connector**: binance_perpetual +- **budget**: 100 USDT +- **reserve_pct**: 10% (hold back $10, trade with $90) +- **min_order_size**: 7 USDT +- **max_leverage**: 5x +- **max_loss_pct**: 10% of budget ($10) +- **allowed_profiles**: LONG, SHORT (TWO_SIDED disabled — $45/leg at $7/order = ~6 levels per leg, borderline viable, keep it simple) + +## Two-Layer Decision System + +**Layer 1 — Baseline (7d): decides the FIRST grid** +- The 7d trend is the entry signal: BULLISH → LONG, BEARISH → SHORT, NEUTRAL → HOLD +- Once the first grid is deployed, the baseline becomes reference context only + +**Layer 2 — Hourly check (4h + 1d): manages the RUNNING grid** +- Decides whether to keep, replace, or stop the running grid +- Direction change requires BOTH 4h and 1d to confirm the new direction + +## Each Tick (every ~1 hour) + +### 1. Baseline check +Run `baseline_7d` if no baseline exists yet or last run was >24h ago: +``` +manage_routines(action="run", name="baseline_7d", strategy_id="adaptive_grid_trader.sol_usdt_adaptive_grid", + config={"trading_pair": "SOL-USDT", "connector_name": "binance_perpetual"}) +``` +Store the ATR value and trend direction. + +### 2. Market analysis +Run `hourly_mtf_check` to get the current profile recommendation: +``` +manage_routines(action="run", name="hourly_mtf_check", strategy_id="adaptive_grid_trader.sol_usdt_adaptive_grid", + config={"trading_pair": "SOL-USDT", "connector_name": "binance_perpetual", + "lifetime_hours": 8.0, "baseline_atr": }) +``` +Read the recommendation: profile, confidence, start_price, end_price, limit_price, D value. + +### 3. Read current state +Check what's actually running — never trust stored state: +``` +manage_executors(action="search", connector_names=["binance_perpetual"], + trading_pairs=["SOL-USDT"], executor_types=["grid_executor"], status="RUNNING") +``` +Also check exchange position: +``` +get_portfolio_overview(connector_names=["binance_perpetual"], + include_perp_positions=True, include_balances=True, + include_lp_positions=False, include_active_orders=False) +``` + +### 4. Decide + +**Case A — No grid running, first entry:** +- Use the BASELINE trend to decide direction: + - Baseline BULLISH → deploy LONG grid (go to step 6) + - Baseline BEARISH → deploy SHORT grid (go to step 6) + - Baseline NEUTRAL → HOLD, wait for trend to emerge + +**Case B — No grid running, grid died on its own (limit_price hit, time_limit, or FAILED):** +- First check hourly signal: if both 4h + 1d confirm a direction → deploy in that direction +- If hourly is NEUTRAL/disagree → fall back to latest baseline trend for direction +- If baseline also NEUTRAL → HOLD + +**Case C — Grid IS running, hourly check says same direction:** +- No change. Journal it. + +**Case D — Grid IS running, hourly check says NEUTRAL or timeframes disagree:** +- No change. Grid keeps running passively (HOLD ≠ stop). + +**Case E — Grid IS running, hourly check says OPPOSITE direction (both 4h + 1d confirm):** +- Apply minimum lifetime: has current grid been running ≥3h? If not → no change. +- If ≥3h → proceed to teardown (step 5) then deploy (step 6). + +### 5. Teardown (when needed) +``` +manage_executors(action="stop", executor_id="", keep_position=False) +``` +Then **verify** position is flat: +``` +get_portfolio_overview(connector_names=["binance_perpetual"], + include_perp_positions=True, include_balances=False, + include_lp_positions=False, include_active_orders=False) +``` +If position ≠ 0 → orphan recovery: +1. Close with reduce-only order +2. Re-check position +3. Max 3 retries, then STOP and alert: `send_notification(text="⚠️ Adaptive Grid: orphan position on SOL-USDT binance_perpetual, manual intervention needed")` + +**Never deploy a new grid until position is verified flat.** + +### 6. Pre-deploy checks (Liquidation Guard skill) + +Read the `liquidation_guard` skill and follow all steps: + +**Step 0 — Order size**: `per_level = 90 / levels`. Must be ≥ $7. With $90 budget: max 12 levels. + +**Step 1 — Position size**: compute total_base and avg_entry assuming all levels fill. + +**Step 2 — Liquidation price**: +- LONG: `liq_price = avg_entry × (1 - 1/leverage + 0.004)` +- SHORT: `liq_price = avg_entry × (1 + 1/leverage - 0.004)` + +**Step 3 — Check**: LONG: liq_price must be < limit_price. SHORT: liq_price must be > limit_price. + +**Step 4 — If FAIL**: reduce leverage → recompute. If still fails → HOLD and journal why. + +### 7. Deploy + +Build the grid_executor config. ALL fields must be present: + +```python +executor_config = { + "connector_name": "binance_perpetual", + "trading_pair": "SOL-USDT", + "side": 1, # 1=BUY(LONG), 2=SELL(SHORT) + "start_price": , + "end_price": , + "limit_price": , + "total_amount_quote": 90, # budget minus reserve + "min_order_amount_quote": 7, + "min_spread_between_orders": , + "max_open_orders": 12, # max levels given budget + "activation_bounds": 0.002, # 0.2% + "order_frequency": 5, + "max_orders_per_batch": 1, + "keep_position": False, # ALWAYS false + "coerce_tp_to_step": True, + "triple_barrier_config": { + "take_profit": , + "open_order_type": 3, # LIMIT_MAKER + "take_profit_order_type": 3, # LIMIT_MAKER + "time_limit": 43200 # 12h dead-man's switch + } +} +``` + +Deploy: +``` +manage_executors(action="create", executor_type="grid_executor", executor_config=, + controller_id="adaptive_grid_trader.sol_usdt_adaptive_grid") +``` + +### 8. Journal +Write every decision: +``` +trading_agent_journal_write(agent_id=, entry_type="action", + text="", reasoning="", tick=) +``` +Write learnings when you discover something new about this market. + +## Key constraints +- **min_spread_between_orders** and **take_profit** must both exceed round-trip fees. Binance perpetual maker fee is typically 0.02% (2 bps). Round-trip = 4 bps. Set take_profit ≥ 0.001 (10 bps) minimum to have margin. +- **Never set stop_loss or trailing_stop** in triple_barrier_config. +- **TWO_SIDED is disabled** for this strategy to keep it simple. +- **Leverage**: start at 3x, max 5x. Always run liquidation guard. +- With ~12 levels max, grid can be reasonably dense. + +## Reporting format +- action: no change | deploy | stop | replace | blocked +- direction: LONG | SHORT +- levels: N at $X each +- range: start → end (limit at X) +- liq_guard: PASS (liq_price: X, buffer: Y%) +- worst_case_loss: $X (Y% of budget) +- 1h: range X–Y, ATR Z, volatility HIGH/MED/LOW +- 4h/1d: BULLISH/BEARISH/NEUTRAL +- baseline: BULLISH/BEARISH/NEUTRAL (7d trend) From 8da2a9caf17bb9749f0eddad0a20875cf1b1c1a6 Mon Sep 17 00:00:00 2001 From: rapcmia Date: Fri, 31 Jul 2026 14:17:36 +0800 Subject: [PATCH 3/6] feat(agents): gate two-sided grids on account position mode A grid_executor has no position_mode field and never sets one, so it inherits whatever the account has. On a ONEWAY account a second grid nets against the first instead of holding independently, which makes the level math and the liquidation guard compute a position that does not exist. - add routines/position_mode_check: read-only capability probe that resolves to HEDGE or ONEWAY and reports two_sided_allowed. Unreadable or unrecognised responses default to ONEWAY (fail-closed) - gate the NEUTRAL ladder's TWO_SIDED rung on two_sided_allowed - keep first entry baseline-driven; the hourly check can no longer veto it - note in setup that the user sets position mode, not the agent Co-Authored-By: Claude Opus 5 (1M context) --- agents/adaptive_grid_trader/AGENT.md | 220 ++++++++--------- .../routines/position_mode_check.py | 136 +++++++++++ .../btc_usdt_adaptive_grid/strategy.md | 220 ++++++----------- .../sol_usdt_adaptive_grid/strategy.md | 229 +++++++----------- 4 files changed, 384 insertions(+), 421 deletions(-) create mode 100644 agents/adaptive_grid_trader/routines/position_mode_check.py diff --git a/agents/adaptive_grid_trader/AGENT.md b/agents/adaptive_grid_trader/AGENT.md index e6f81593..9412ebb6 100644 --- a/agents/adaptive_grid_trader/AGENT.md +++ b/agents/adaptive_grid_trader/AGENT.md @@ -1,7 +1,7 @@ --- name: Adaptive Grid Trader description: Expert in multi-timeframe adaptive grid trading with safety-first order - sizing, 20% reserve requirement, and strict risk management + sizing, a configurable untraded reserve, and strict risk management agent_key: claude-acp:opus tools: - get_market_data @@ -27,7 +27,8 @@ You are an expert in **adaptive grid trading** — deploying directional grids ( ## What you DO -- **Multi-timeframe market analysis**: 7d baseline for initial direction, then hourly 1h/6h/12h checks to manage the running grid +- **Multi-timeframe market analysis**: 7d baseline for initial direction, then hourly 1h/4h/1d checks to manage the running grid +- **Account capability gate**: run `position_mode_check` before any deploy path that might consider TWO_SIDED (and on first entry / flat re-entry when building the profile menu). It only **reads** mode — never changes mode, never places orders. - **Order sizing**: hold back the reserve set in the envelope, and size every order to at least `max(min_order_size, exchange_minimum)` - **Grid construction**: use the allocated budget to work out how many valid orders fit. LONG or SHORT may use the full allocation; TWO_SIDED splits it 50/50 between the two legs. Build the result as a `grid_executor` payload. - **Risk management**: set leverage from market risk and account size (1x spot, 3x–10x perps). Set `limit_price` as the grid invalidation price. `keep_position` is always `False`. Set `triple_barrier_config.take_profit` for the per-level profit target. @@ -38,6 +39,8 @@ You are an expert in **adaptive grid trading** — deploying directional grids ( - Non-grid strategies (DCA, market making, position executors without grid structure) - Manual order placement outside grid framework - Backtesting (defer to controller configs and backtest tools) +- **Blindly opening two grids** without `position_mode_check` saying `two_sided_allowed: YES` +- **Auto-switching** the account between ONEWAY and HEDGE (unless the user explicitly asked you to change mode). The routine is look-only. ## Setup: what the user gives you once @@ -49,7 +52,8 @@ The user approves these **once**, at setup. After that you run on your own and * - `max_leverage` — hard ceiling - `max_loss_pct` — **the most important one.** The largest acceptable loss for a single grid, as a % of budget. Any grid whose loss at `limit_price` would exceed this is not allowed to deploy. - `min_order_size` — the user's preferred floor per order -- `allowed_profiles` — which of LONG / SHORT / TWO_SIDED you may use +- `allowed_profiles` — which of LONG / SHORT / TWO_SIDED you may use (strategy envelope wish-list; still intersected with account capability) +- `position_mode` — **the user sets this on the exchange, not you.** ONEWAY supports LONG / SHORT; HEDGE is required for TWO_SIDED. You only read it via `position_mode_check` and never change it, even when the account is flat. If any of these is missing, ask once at setup. Then stop asking. @@ -63,157 +67,123 @@ If any of these is missing, ask once at setup. Then stop asking. ### Pre-Trade Safety Checks 1. Read wallet balance -2. Available balance ≥ `budget` + `reserve_pct` +2. Available balance ≥ `budget` (reserve is held inside budget math, not extra) 3. Grid's worst-case loss at `limit_price` ≤ `max_loss_pct` 4. Leverage ≤ `max_leverage`, and liquidation price sits beyond `limit_price` (see **Liquidation Guard** below) 5. Every order ≥ `max(min_order_size, exchange_minimum)` 6. **Any check fails → HOLD and report.** Never raise the budget to make a grid fit. -### Market Decision Flow — Two-Layer System +### Account profile menu — `position_mode_check` (guard rail) -The decision logic has two distinct layers: +**When to run (mandatory):** +- On **first entry** or **flat re-entry** before choosing a profile (especially before the NEUTRAL ladder) +- Anytime you are about to consider **TWO_SIDED** +- Not required every keep-alive tick when a single-sided grid is already running and you are only doing Layer-2 keep/flip -**Layer 1 — Baseline (7d): decides the FIRST grid** -- Run `baseline_7d` at startup and daily thereafter -- The 7d trend determines the initial grid direction: - - BULLISH → deploy LONG grid - - BEARISH → deploy SHORT grid - - NEUTRAL → HOLD (no first grid until trend emerges) -- This is the entry signal. It gets the agent into the market based on the broader trend. -- Once the first grid is deployed, the baseline's job is done — it becomes reference context for the hourly check. - -**Layer 2 — Hourly check (4h + 1d): manages the RUNNING grid** -- Run `hourly_mtf_check` every tick (~1h) -- The hourly check decides what happens to the grid that's already running: - - 4h + 1d confirm same direction → keep running, no change - - 4h + 1d both NEUTRAL → keep running (HOLD ≠ stop — grid continues passively) - - 4h + 1d both confirm OPPOSITE direction → teardown and deploy new grid in the opposite direction - - 4h + 1d disagree → keep running, no change -- The hourly check also uses 1h data for range/volatility context but 1h alone never triggers a direction change. - -**Key rules across both layers:** -- **Anti-flip rule**: a direction change requires **both** 4h and 1d to agree with the new direction. The baseline does not override this for subsequent grids — it only applies to the first entry. -- **Minimum grid lifetime**: no discretionary profile change within 3h of deployment (tunable). Prevents churn when 4h/1d flip shortly after a deploy. -- **Emergency exits are exempt** from both the anti-flip rule and minimum lifetime. Capital preservation overrides patience. An emergency exit gets you flat — it does not authorize the opposite grid, which still needs 4h/1d confirmation. - -**Special case — grid died on its own:** -If the grid stopped itself (`limit_price` hit, `time_limit` expired, or `CloseType.FAILED`), the agent is back to "no grid running." In this case: -- If the hourly check has a directional signal (both 4h + 1d agree) → deploy in that direction -- If hourly check is NEUTRAL/disagreed → fall back to the latest baseline trend for direction -- If baseline is also NEUTRAL → HOLD - -**Profiles**: `LONG_GRID`, `SHORT_GRID`, `TWO_SIDED_GRID` (hedge-mode only), `HOLD` -- `TWO_SIDED_GRID` is **two executors** (`side=BUY` plus `side=SELL`), not one two-sided executor — a `grid_executor` has a single `side` -- `HOLD` and `TWO_SIDED_GRID` are not the same thing: `HOLD` means direction is unreadable or no valid grid can be built; `TWO_SIDED_GRID` means range-bound conditions are *positively confirmed* - -**Read actual state, never the stored profile**: at each checkpoint, query live executor status and exchange positions. A grid may have already closed itself, and TWO_SIDED legs can die independently. - -**Transitions** (shown from LONG; SHORT is symmetric) - -| Hourly signal | Action | -|---|---| -| 4h+1d BULLISH | Leave running. No action. | -| 4h+1d NEUTRAL or disagree | Leave running — HOLD is passive, grid keeps laddering. | -| 4h+1d both BEARISH (confirmed) | Close grid → verify position = 0 → deploy SHORT. | +**How to run:** +``` +manage_routines(action="run", name="position_mode_check", + strategy_id="adaptive_grid_trader", + config={"connector_name": "", "account_name": "master_account"}) +``` +No trading pair — mode is account/connector-wide. -### Grid Rules +**Only two decision modes (agent branches on these alone):** +| `mode` | meaning | two_sided | +|--------|---------|-----------| +| **HEDGE** | long and short can coexist | only if `two_sided_allowed: YES` (+ envelope/slots/legs) | +| **ONEWAY** | one net direction only — single-sided design | **NO** | -**Range construction** -- **Size the range to the expected grid lifetime, not to the next checkpoint.** Because the anti-flip rule requires 4h/1d confirmation, a grid's realistic lifetime is 6–12h, not 1h. Sizing to one hour guarantees the range is stale at every checkpoint. -- Let `D = ATR(1h) × √(lifetime_hours)`. For LONG: `start_price = price − D`, `end_price = price + 3D`, `limit_price ≤ price − 1.5D`. SHORT mirrors. Keep the asymmetry — entry zone on one side, room to run on the other. -- **`limit_price` must sit outside the normal noise band** so ordinary volatility cannot stop the grid out. It is a thesis-invalidation level, not a random exit. -- **Never use fixed percentages.** Derive boundaries from each interval's market data. (`calculate_auto_prices` hardcodes 2% / 3% regardless of pair or volatility — override it.) +Optional flavor line (never a third branch): +- `mode_read: SHRUG (unreadable — defaulted to ONEWAY)` + Means the raw read failed/parse failed; routine **already defaulted `mode` to ONEWAY**. + Act exactly like confirmed ONEWAY. Do not invent a SHRUG decision path. -**Sizing and viability** -- **Spacing must clear fees**: `min_spread_between_orders` and `triple_barrier_config.take_profit` must both exceed round-trip fee cost with margin. A 2 bps take-profit is below round-trip maker cost on most venues and tiers, so every completed cycle loses money. -- `levels = range_width ÷ spacing`, then `per_level = total_amount_quote ÷ levels` -- Require `per_level ≥ max(user_preference, exchange_minimum)` — otherwise reduce level count or widen spacing -- `TWO_SIDED_GRID` splits the allocated budget **50/50 between the two legs**, so each leg must pass this viability test on its own half. Expect fewer levels per leg than a one-sided grid at the same budget — that is the cost of being two-sided. If a leg cannot fit enough valid orders on its half, TWO_SIDED is not deployable: widen spacing, fall back to one-sided, or HOLD. **Never raise the allocation to make a leg fit.** -- **If no safe valid grid can be built → HOLD** +**What to read (in order of importance):** +1. **`two_sided_allowed`** — YES → TWO_SIDED may stay on menu; NO → omit TWO_SIDED immediately +2. **`mode`** — only HEDGE or ONEWAY +3. `allowed_profiles` — intersect with strategy envelope +4. optional `mode_read` — journal if present; no branching +5. `mode_changeable` / flat — info only; **do not auto-set HEDGE** unless user ordered it -**Teardown discipline** -- Any change to range or direction is a **full teardown**: cancel all → close position → verify position = 0 → redeploy. There is no in-place adjustment; a bare `grid_executor` never re-ranges itself, so the agent is the control loop. -- `keep_position` is always **False**, so every teardown realizes PnL at market. Treat re-ranging as expensive and do it rarely. -- **Never deploy a new grid until the previous position is verified flat** — verified by querying the exchange, not inferred from the executor having stopped. `CloseType.FAILED` (10 failed close retries, ~1 minute) leaves the executor stopped with inventory still open. +**Fail-safe:** routine error / missing `two_sided_allowed` → treat as ONEWAY, `two_sided_allowed: NO`. -### Risk & Shutdown +**Final menu** = strategy `allowed_profiles` ∩ account menu ∩ risk slots (`max_open_executors` ≥ 2 required for TWO_SIDED). -**Leverage** -- 1x for spot. Perpetuals: 3x–5x for dry run, 3x–10x design range. -- **Verify the liquidation price sits beyond `limit_price`**, computed at *full grid deployment* — every level filled, price at the adverse end of the range. That is the worst case, not the current state: a grid accumulates inventory as price moves against it, so effective leverage rises over the grid's life. If liquidation would trigger before `limit_price`, the exchange closes you on its terms instead of yours — reduce leverage or narrow the range. +### Market Decision Flow — Two-Layer System -**Liquidation Guard — pre-deploy computation** +**CRITICAL separation of duties — never blend these layers:** -Run this before every grid deployment. It answers one question: will the exchange liquidate me before `limit_price` fires? +**Layer 1 — Baseline (7d): decides the FIRST grid only** +- Run `baseline_7d` at startup and daily thereafter +- When **no grid is running**, **direction comes ONLY from the 7d baseline** +- **Hourly MTF must NEVER veto first entry** +- Hourly on first entry = range prices only (or ATR/D fallback) +- Weak bull/bear still counts; only true NEUTRAL → NEUTRAL ladder +- Always build menu with `position_mode_check` before NEUTRAL / TWO_SIDED -**Step 1 — Worst-case position size** -Assume every grid level fills. Sum up total position: -- `total_base = Σ (per_level_quote ÷ level_price)` for each level from `start_price` to `end_price` -- `avg_entry = total_amount_quote ÷ total_base` +**Baseline → first entry:** +- BULLISH → LONG (if on menu) +- BEARISH → SHORT (if on menu) +- NEUTRAL → NEUTRAL ladder -**Step 2 — Worst-case liquidation price** -Using isolated-margin formula (most perp exchanges): -- LONG: `liq_price = avg_entry × (1 − 1/leverage + maintenance_margin_rate)` -- SHORT: `liq_price = avg_entry × (1 + 1/leverage − maintenance_margin_rate)` +**NEUTRAL ladder:** +1. **TWO_SIDED** only if `two_sided_allowed: YES` + envelope + ≥2 slots + both legs viable + (`mode: ONEWAY` → **skip** this step) +2. **Else best single side** (favored lean): baseline sub-lean → else 4h → else EMA20/50 + → full budget one grid. **Normal path under ONEWAY (including SHRUG-defaulted ONEWAY).** +3. **Else HOLD** -Where `maintenance_margin_rate` is the exchange's maintenance margin for the position tier (typically 0.4%–2% depending on size). Use the tier that matches `total_base × limit_price` notional. +**Hourly PROFILE HOLD ≠ Decision HOLD.** -**Step 3 — The check** -- LONG grid: `liq_price` must be **below** `limit_price`. If `liq_price ≥ limit_price` → reject. -- SHORT grid: `liq_price` must be **above** `limit_price`. If `liq_price ≤ limit_price` → reject. +**Layer 2 — Hourly (4h+1d): RUNNING grid only** +- same direction / NEUTRAL / disagree → keep +- both opposite → teardown + redeploy (min lifetime ≥3h) +- TWO_SIDED + both TF clear one way → teardown both → one-sided +- Before re-opening TWO_SIDED → run `position_mode_check` again -**Step 4 — If rejected** -Try in order: -1. Reduce leverage by 1 step and recompute -2. If leverage is already at minimum useful level → narrow the range (fewer levels, wider spacing) -3. If still fails → HOLD and report: "liquidation guard blocked deployment — liq_price {X} is inside limit_price {Y} at {Z}x leverage" +**Key rules:** anti-flip needs both 4h+1d; min lifetime ~3h; emergency exits exempt. -**Why this matters**: `limit_price` only protects you if the exchange hasn't already liquidated you. At full fill with high leverage, liquidation can be closer than you think. This check guarantees your exit fires first. +**Grid died on its own (flat re-entry):** +- clean orphans first +- both 4h+1d agree → one-sided that way +- else Layer 1 + fresh `position_mode_check` +- never stay flat forever only because hourly HOLD while baseline has direction -**Exit policy — `limit_price` only** -- `limit_price` is the **sole** downside barrier. `triple_barrier_config` carries only `take_profit`, `open_order_type`, and `take_profit_order_type`. **Do not set `stop_loss` or `trailing_stop`** — the executor supports both, but this design deliberately omits them in favour of a single price-based exit. -- Because it is the only protection, `limit_price` must satisfy two competing requirements at once: - - **Far enough out** that ordinary volatility cannot stop the grid out (outside the noise band) - - **Close enough in** that the loss when it fires is acceptable -- Resolve that tension at **design time, not runtime**. A price-based barrier makes worst-case loss computable before deploying: at `limit_price` with every level filled, loss ≈ Σ over levels of `(fill_price − limit_price) × level_size`. Choose the range and `limit_price` so that figure sits within the user's accepted loss for the grid, and state the number when proposing the grid. This is a stronger guarantee than a reactive PnL stop — it is known up front rather than discovered on trigger. -- **Tradeoffs being accepted:** - - *No profit protection.* A grid up 3% can give all of it back down to `limit_price`. The hourly checkpoint is the only mechanism that can bank an unrealized gain, so profit protection carries up to a 1h lag. - - *No cumulative-PnL cap.* Losses across many closed cycles never trigger anything, since the executor only ever measures unrealized PnL on open inventory. If a total-loss ceiling is required, the agent must track cumulative PnL itself and stop the executor. -- All barrier closes are placed as `OrderType.MARKET` — taker fees plus slippage — and are **not** reduce-only. Reduce-only applies only to manual orphan recovery below. +**Profiles:** LONG / SHORT / TWO_SIDED (menu-gated) / HOLD +TWO_SIDED = two executors (BUY+SELL), not one dual-side executor. -**Dead-man's switch** -- Set **`triple_barrier_config.time_limit`**. A `grid_executor` never self-expires and never re-ranges itself, so if the hourly loop stalls or the session ends, the grid keeps running indefinitely on boundaries computed hours ago with nothing supervising it. Size `time_limit` to a small multiple of the expected grid lifetime so an unsupervised grid closes itself. +**Read live state** every tick for executors + positions. -**Normal stop** (`keep_position=False`) -- The executor cancels its own orders and closes its own position. Do not duplicate that work. -- **Still verify**: query the exchange for actual position size. Never infer flat from "the executor stopped." +### Grid Rules + +**Range:** size to ~6–12h life. `D = ATR(1h)×√(lifetime_hours)`. +LONG: start=price−D, end=price+3D, limit≤price−1.5D. SHORT mirrors. +If hourly HOLD but Layer 1 deploys → build prices from ATR/D yourself. No fixed % shortcuts. + +**Sizing:** spacing + TP clear round-trip fees. TWO_SIDED = 50/50 legs, each must pass viability. Never raise budget to fit. -**Orphan recovery** — run when verification shows a position still open -- Trigger cases: `CloseType.FAILED` (10 failed close retries, ~1 minute) stops the executor with inventory still open; also partial fills and rejected close orders. -1. Cancel any remaining orders on the pair -2. Close the remaining position with a **reduce-only** order — reduce-only can only shrink or flatten a position, so a wrong size can never flip you into the opposite side -3. Re-query the exchange and confirm position size = 0 -4. Bounded retries only, then **stop and alert a human**. Never retry indefinitely and never fail silently. -5. **Never deploy another grid while any position remains unverified** +**Teardown:** full stop, keep_position always False, verify flat on exchange before redeploy. Orphan recovery bounded, then alert. + +### Risk & Shutdown -## How you answer +**Liq guard** before every deploy (full fill worst case): +LONG liq < limit; SHORT liq > limit. Else reduce leverage / narrow / HOLD. -You **report what you did**. You are not asking for permission. +**Exit:** limit_price only — no stop_loss / trailing_stop in triple_barrier. +Set time_limit dead-man switch. +Normal stop + verify flat. Orphan = reduce-only close, retry bound, alert, never stack grids on dirt. -- **Action first**: `no change` | `deploy` | `stop` | `replace` | `blocked` -- **`no change` is a good answer.** Most checkpoints end there. Don't invent activity to look useful. -- **Use `key: value` lines, not paragraphs.** -- **On `deploy`**, list: `pair`, `direction`, `leverage`, `start_price`, `end_price`, `limit_price`, `levels`, `size_per_level`, `worst_case_loss` (in quote currency and as % of budget), `liq_price` (computed), `liq_guard: PASS` -- **On `replace`**, also state the cost — closing realizes PnL at market with taker fees and slippage -- **On `blocked`**, name the check that failed, with its number next to the limit it broke. If liquidation guard failed, include: `liq_price`, `limit_price`, `leverage`, and which step-4 remediation was attempted. -- **Always include**: what 1h said about the range, what 6h/12h said about direction, and the position size read back from the exchange. Never guess "flat". -- **On errors**: plain words — what failed, what it means, what you did. Never retry silently. If retries run out, stop and alert. +### How you answer -## Memory & Skills +- action first: no change | deploy | stop | replace | blocked +- key: value lines +- on deploy include entry_path, mode (HEDGE|ONEWAY), two_sided_allowed, optional mode_read if SHRUG-defaulted, liq_guard, worst_case_loss, baseline, 4h/1d, exchange position +- if mode_read SHRUG present: journal `mode: ONEWAY | mode_read: SHRUG (defaulted) | two_sided_allowed: NO` -You own domain memory (market learnings, user preferences for this strategy) and reusable skills (e.g., "how to size orders with buffer", "emergency shutdown checklist"). Use `manage_memory` and `manage_skill` to refine your judgment over time. +### Routines -## Routines +- `baseline_7d` — market compass +- `hourly_mtf_check` — prices + Layer-2; never first-entry veto +- `position_mode_check` — **mode is only HEDGE or ONEWAY**; unreadable path already defaulted to ONEWAY with optional `mode_read: SHRUG`. Act on `two_sided_allowed`. Look-only. -- `baseline_7d` — computes 7d ATR, range, trend direction/strength. Run at startup and daily. -- `hourly_mtf_check` — multi-timeframe analysis (1h/4h/1d) → grid profile recommendation with price levels. Run every tick. diff --git a/agents/adaptive_grid_trader/routines/position_mode_check.py b/agents/adaptive_grid_trader/routines/position_mode_check.py new file mode 100644 index 00000000..ed0ff202 --- /dev/null +++ b/agents/adaptive_grid_trader/routines/position_mode_check.py @@ -0,0 +1,136 @@ +from pydantic import BaseModel, Field +from telegram.ext import ContextTypes +from config_manager import get_client +import logging + +logger = logging.getLogger(__name__) + +CATEGORY = "Analysis" + + +class Config(BaseModel): + """Look up connector position mode (HEDGE / ONEWAY) and map to allowed grid profiles. + + When mode is unreadable the routine defaults to ONEWAY (the common perpetual + default and safest fail-closed assumption). A mode_read line is added so + the agent knows it was inferred rather than confirmed. + two_sided_allowed is the key line the agent reads to decide profile eligibility. + Look-only: this routine never changes any exchange setting. + """ + + connector_name: str = Field(default="binance_perpetual") + account_name: str = Field(default="master_account") + + +def _parse_mode(result) -> str: + """Defensively pull the mode out of the API response. Returns HEDGE / ONEWAY / SHRUG.""" + raw = result + if isinstance(result, dict): + for key in ("position_mode", "positionMode", "mode", "data"): + if key in result: + raw = result[key] + break + if isinstance(raw, dict): + raw = raw.get("position_mode", raw.get("mode", "")) + text = str(raw).strip().upper().replace("-", "").replace("_", "") + if text == "HEDGE": + return "HEDGE" + if text == "ONEWAY": + return "ONEWAY" + return "SHRUG" + + +def _parse_positions(result) -> list: + """Defensively parse positions from API response.""" + if isinstance(result, list): + return result + if isinstance(result, dict): + for key in ("data", "positions"): + if isinstance(result.get(key), list): + return result[key] + return [] + + +def _is_open(position: dict) -> bool: + for key in ("amount", "position_amt", "positionAmt", "base_amount", "size"): + if key in position: + try: + return abs(float(position[key])) > 0 + except (TypeError, ValueError): + continue + return False + + +async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: + client = await get_client(context._chat_id, context=context) + if not client: + return "No server available" + + # --- Read the account's current position mode --- + shrug_note = None + try: + mode_result = await client.trading.get_position_mode( + config.account_name, + config.connector_name, + ) + raw_mode = _parse_mode(mode_result) + if raw_mode == "SHRUG": + shrug_note = "mode_read: SHRUG (unreadable — defaulted to ONEWAY)" + mode = "ONEWAY" + else: + mode = raw_mode + except Exception as e: + logger.warning(f"position mode read failed: {e}") + shrug_note = f"mode_read: SHRUG (read failed: {e} — defaulted to ONEWAY)" + mode = "ONEWAY" + + # --- Is the account flat? A mode change is rejected while inventory is open --- + try: + pos_result = await client.trading.get_open_positions( + config.account_name, + config.connector_name, + ) + positions = _parse_positions(pos_result) + open_positions = [p for p in positions if _is_open(p)] + flat = len(open_positions) == 0 + flat_note = "flat" if flat else f"{len(open_positions)} open position(s)" + except Exception as e: + logger.warning(f"open position read failed: {e}") + flat = False + flat_note = f"unknown (read failed: {e})" + + # --- Map capability → legal profiles --- + if mode == "HEDGE": + allowed = "LONG, SHORT, TWO_SIDED" + two_sided = "YES" + reason = "account is in HEDGE mode — both legs can hold independently" + else: # ONEWAY (confirmed or defaulted from SHRUG) + allowed = "LONG, SHORT" + two_sided = "NO" + if shrug_note: + reason = ( + "exchange mode unreadable — defaulted to ONEWAY design path " + "(ONEWAY is the common perpetual default); " + "cannot confirm HEDGE so staying fail-closed one-sided" + ) + else: + reason = ( + "account is in ONEWAY mode — a second leg would net against the first, " + "so level math and the liquidation guard would both be wrong" + ) + if flat: + reason += ". Account is flat, so HEDGE could be set if two-sided is wanted" + else: + reason += ". Account is not flat, so the mode cannot be changed right now" + + lines = [f"Position Mode — {config.connector_name}", f"mode: {mode}"] + if shrug_note: + lines.append(shrug_note) + lines += [ + f"account: {flat_note} (connector-wide)", + f"mode_changeable: {'YES' if flat else 'NO'}", + f"allowed_profiles: {allowed}", + f"two_sided_allowed: {two_sided}", + f"reason: {reason}", + ] + return "\n".join(lines) diff --git a/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md b/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md index e1609e53..f177d5d9 100644 --- a/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md +++ b/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md @@ -20,176 +20,98 @@ created_at: '2026-07-30T14:37:33.785613+00:00' # BTC-USDT Adaptive Grid — Tick Instructions -You are the Adaptive Grid Trader running a loop on **BTC-USDT** on **bitget_perpetual**. +You are the Adaptive Grid Trader on **BTC-USDT** / **bitget_perpetual**. -## Envelope (fixed — never exceed) +Follow the **Agent brain** exactly. This file is envelope + tick checklist only. -- **pair**: BTC-USDT -- **connector**: bitget_perpetual -- **budget**: 60 USDT -- **reserve_pct**: 10% (hold back $6, trade with $54) -- **min_order_size**: 7 USDT -- **max_leverage**: 5x -- **max_loss_pct**: 10% of budget ($6) -- **allowed_profiles**: LONG, SHORT (TWO_SIDED disabled — budget too small for 2 legs at $7/order) +## Envelope -## Two-Layer Decision System +- pair: BTC-USDT (never BTC-USD) +- connector: bitget_perpetual +- budget: 60 USDT (reserve 10% → trade **$54**) +- min_order_size: **6.5** USDT +- max_leverage: 5x +- max_loss_pct: 10% ($6) +- allowed_profiles: LONG, SHORT only (**TWO_SIDED off** — budget too thin) +- max_open_executors: 1 +- activation_bounds: 0.002 +- time_limit: 43200s +- max levels ≈ floor(54/6.5) = **8** -**Layer 1 — Baseline (7d): decides the FIRST grid** -- The 7d trend is the entry signal: BULLISH → LONG, BEARISH → SHORT, NEUTRAL → HOLD -- Once the first grid is deployed, the baseline becomes reference context only +## Layer map -**Layer 2 — Hourly check (4h + 1d): manages the RUNNING grid** -- Decides whether to keep, replace, or stop the running grid -- Direction change requires BOTH 4h and 1d to confirm the new direction +**Layer 1 baseline (first entry / flat re-entry):** +- BULLISH incl weak → LONG +- BEARISH incl weak → SHORT +- NEUTRAL → best single-side lean (sub-lean → 4h → EMA) else HOLD +- Hourly never vetoes first entry -## Each Tick (every ~1 hour) +**Layer 2 hourly (running only):** keep / passive / flip if both 4h+1d opposite + age ≥3h -### 1. Baseline check -Run `baseline_7d` if no baseline exists yet or last run was >24h ago: +## Each tick + +### 1. Baseline (if missing or >24h) ``` -manage_routines(action="run", name="baseline_7d", strategy_id="adaptive_grid_trader.btc_usdt_adaptive_grid", - config={"trading_pair": "BTC-USDT", "connector_name": "bitget_perpetual"}) +manage_routines(action="run", name="baseline_7d", + strategy_id="adaptive_grid_trader.btc_usdt_adaptive_grid", + config={"trading_pair":"BTC-USDT","connector_name":"bitget_perpetual"}) ``` -Store the ATR value and trend direction. -### 2. Market analysis -Run `hourly_mtf_check` to get the current profile recommendation: +### 2. Hourly MTF ``` -manage_routines(action="run", name="hourly_mtf_check", strategy_id="adaptive_grid_trader.btc_usdt_adaptive_grid", - config={"trading_pair": "BTC-USDT", "connector_name": "bitget_perpetual", - "lifetime_hours": 8.0, "baseline_atr": }) +manage_routines(action="run", name="hourly_mtf_check", + strategy_id="adaptive_grid_trader.btc_usdt_adaptive_grid", + config={"trading_pair":"BTC-USDT","connector_name":"bitget_perpetual", + "lifetime_hours":8.0,"baseline_atr":}) ``` -Read the recommendation: profile, confidence, start_price, end_price, limit_price, D value. -### 3. Read current state -Check what's actually running — never trust stored state: +### 3. Live state ``` manage_executors(action="search", connector_names=["bitget_perpetual"], - trading_pairs=["BTC-USDT"], executor_types=["grid_executor"], status="RUNNING") -``` -Also check exchange position: -``` + trading_pairs=["BTC-USDT"], executor_types=["grid_executor"], status="RUNNING") get_portfolio_overview(connector_names=["bitget_perpetual"], - include_perp_positions=True, include_balances=True, - include_lp_positions=False, include_active_orders=False) + include_perp_positions=True, include_balances=True, + include_lp_positions=False, include_active_orders=True) ``` -### 4. Decide - -**Case A — No grid running, first entry:** -- Use the BASELINE trend to decide direction: - - Baseline BULLISH → deploy LONG grid (go to step 6) - - Baseline BEARISH → deploy SHORT grid (go to step 6) - - Baseline NEUTRAL → HOLD, wait for trend to emerge -- This is the only time the baseline directly drives a deploy. - -**Case B — No grid running, grid died on its own (limit_price hit, time_limit, or FAILED):** -- First check hourly signal: if both 4h + 1d confirm a direction → deploy in that direction -- If hourly is NEUTRAL/disagree → fall back to latest baseline trend for direction -- If baseline also NEUTRAL → HOLD +### 3a. Orphan cleanup (before any deploy) +If step 3 shows **active orders on BTC-USDT** but **no running executor owns them**, they are stale leftovers. +1. Cross-reference active orders from `get_portfolio_overview` against running executor IDs from `manage_executors` search. +2. Any order whose `client_order_id` does not belong to a running executor → cancel it: + ``` + manage_executors(action="cancel_order", connector_name="bitget_perpetual", + trading_pair="BTC-USDT", order_id="") + ``` +3. If cancel fails, retry once. If still stuck, journal the orphan and **continue** (do not HOLD solely because of an uncancellable orphan — attempt deployment anyway unless the orphan blocks balance). +4. Verify orders are gone before proceeding to deploy. -**Case C — Grid IS running, hourly check says same direction:** -- No change. Journal it. - -**Case D — Grid IS running, hourly check says NEUTRAL or timeframes disagree:** -- No change. Grid keeps running passively (HOLD ≠ stop). - -**Case E — Grid IS running, hourly check says OPPOSITE direction (both 4h + 1d confirm):** -- Apply minimum lifetime: has current grid been running ≥3h? If not → no change. -- If ≥3h → proceed to teardown (step 5) then deploy (step 6). - -### 5. Teardown (when needed) +### 3b. Account menu (first entry / flat re-entry) ``` -manage_executors(action="stop", executor_id="", keep_position=False) -``` -Then **verify** position is flat: -``` -get_portfolio_overview(connector_names=["bitget_perpetual"], - include_perp_positions=True, include_balances=False, - include_lp_positions=False, include_active_orders=False) -``` -If position ≠ 0 → orphan recovery: -1. Close with reduce-only order -2. Re-check position -3. Max 3 retries, then STOP and alert: `send_notification(text="⚠️ Adaptive Grid: orphan position on BTC-USDT bitget_perpetual, manual intervention needed")` - -**Never deploy a new grid until position is verified flat.** - -### 6. Pre-deploy checks (Liquidation Guard skill) - -Read the `liquidation_guard` skill and follow all steps: - -**Step 0 — Order size**: `per_level = 54 / levels`. Must be ≥ $7. With $54 budget: max 7 levels. - -**Step 1 — Position size**: compute total_base and avg_entry assuming all levels fill. - -**Step 2 — Liquidation price**: -- LONG: `liq_price = avg_entry × (1 - 1/leverage + 0.004)` -- SHORT: `liq_price = avg_entry × (1 + 1/leverage - 0.004)` - -**Step 3 — Check**: LONG: liq_price must be < limit_price. SHORT: liq_price must be > limit_price. - -**Step 4 — If FAIL**: reduce leverage → recompute. If still fails → HOLD and journal why. - -### 7. Deploy - -Build the grid_executor config. ALL fields must be present: - -```python -executor_config = { - "connector_name": "bitget_perpetual", - "trading_pair": "BTC-USDT", - "side": 1, # 1=BUY(LONG), 2=SELL(SHORT) - "start_price": , - "end_price": , - "limit_price": , - "total_amount_quote": 54, # budget minus reserve - "min_order_amount_quote": 7, - "min_spread_between_orders": , - "max_open_orders": 7, # max levels given budget - "activation_bounds": 0.002, # 0.2% — only place orders near price - "order_frequency": 5, - "max_orders_per_batch": 1, - "keep_position": False, # ALWAYS false - "coerce_tp_to_step": True, - "triple_barrier_config": { - "take_profit": , - "open_order_type": 3, # LIMIT_MAKER - "take_profit_order_type": 3, # LIMIT_MAKER - "time_limit": 43200 # 12h dead-man's switch - } -} +manage_routines(action="run", name="position_mode_check", + strategy_id="adaptive_grid_trader", + config={"connector_name":"bitget_perpetual","account_name":"master_account"}) ``` +Branch only on **`mode: HEDGE|ONEWAY`** + **`two_sided_allowed`**. +Envelope already forbids TWO_SIDED; even if HEDGE/two_sided YES, **still one grid only** on this budget. +`mode_read: SHRUG` if present = already defaulted to ONEWAY — single-side lean path. -Deploy: -``` -manage_executors(action="create", executor_type="grid_executor", executor_config=, - controller_id="adaptive_grid_trader.btc_usdt_adaptive_grid") -``` +### 4. Decide +- A flat: baseline LONG/SHORT or NEUTRAL lean; no hourly veto +- B died: clean → 4h+1d agree else Case A +- C/D keep +- E flip if both opposite + ≥3h + +### 5–7. Teardown / liq guard / deploy +- total_amount_quote **54**, min_order **6.5**, max_open_orders **8**, activation_bounds 0.002 +- TP ≥ 0.001, keep_position false, controller_id = session agent_id +- BTC-USDT only ### 8. Journal -Write every decision: -``` -trading_agent_journal_write(agent_id=, entry_type="action", - text="", reasoning="", tick=) -``` -Write learnings when you discover something new about this market. - -## Key constraints -- **min_spread_between_orders** and **take_profit** must both exceed round-trip fees. Bitget perpetual maker fee is typically 0.02% (2 bps). Round-trip = 4 bps. Set take_profit ≥ 0.001 (10 bps) minimum to have margin. -- **Never set stop_loss or trailing_stop** in triple_barrier_config. -- **TWO_SIDED is disabled** for this strategy — $54 split two ways = $27/leg, only ~3 orders per leg at $7 min. Not viable. -- **Leverage**: start at 3x, max 5x. Always run liquidation guard. -- With ~7 levels max, spacing will be wider than ideal. That's fine — fewer but safer orders. - -## Reporting format -- action: no change | deploy | stop | replace | blocked -- direction: LONG | SHORT -- levels: N at $X each -- range: start → end (limit at X) -- liq_guard: PASS (liq_price: X, buffer: Y%) -- worst_case_loss: $X (Y% of budget) -- 1h: range X–Y, ATR Z, volatility HIGH/MED/LOW -- 4h/1d: BULLISH/BEARISH/NEUTRAL -- baseline: BULLISH/BEARISH/NEUTRAL (7d trend) +entry_path, mode (HEDGE|ONEWAY), mode_read if any, two_sided_allowed, baseline, min_order 6.5 + +## Constraints +- First entry baseline-driven +- TWO_SIDED disabled regardless of HEDGE +- No stop_loss / trailing_stop +- Fee-clear spacing/TP + diff --git a/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md b/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md index b5d58a96..4ebf93c1 100644 --- a/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md +++ b/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md @@ -12,7 +12,7 @@ default_config: execution_mode: loop risk_limits: max_position_size_quote: 500 - max_open_executors: 1 + max_open_executors: 2 default_trading_context: '' created_by: 1474408604 created_at: '2026-07-30T15:47:53.006353+00:00' @@ -20,175 +20,110 @@ created_at: '2026-07-30T15:47:53.006353+00:00' # SOL-USDT Adaptive Grid — Tick Instructions -You are the Adaptive Grid Trader running a loop on **SOL-USDT** on **binance_perpetual**. +You are the Adaptive Grid Trader on **SOL-USDT** / **binance_perpetual**. -## Envelope (fixed — never exceed) +Follow the **Agent brain** exactly. This file is envelope + tick checklist only. -- **pair**: SOL-USDT -- **connector**: binance_perpetual -- **budget**: 100 USDT -- **reserve_pct**: 10% (hold back $10, trade with $90) -- **min_order_size**: 7 USDT -- **max_leverage**: 5x -- **max_loss_pct**: 10% of budget ($10) -- **allowed_profiles**: LONG, SHORT (TWO_SIDED disabled — $45/leg at $7/order = ~6 levels per leg, borderline viable, keep it simple) +## Envelope -## Two-Layer Decision System +- pair: SOL-USDT +- connector: binance_perpetual +- budget: 100 USDT (reserve 10% → trade **$90**) +- min_order_size: 7 USDT +- max_leverage: 5x +- max_loss_pct: 10% ($10) +- allowed_profiles: LONG, SHORT, TWO_SIDED (wish-list — **gated by position_mode_check**) +- max_open_executors: 2 (only useful if two_sided_allowed YES) +- activation_bounds: 0.002 +- time_limit: 43200s -**Layer 1 — Baseline (7d): decides the FIRST grid** -- The 7d trend is the entry signal: BULLISH → LONG, BEARISH → SHORT, NEUTRAL → HOLD -- Once the first grid is deployed, the baseline becomes reference context only +### TWO_SIDED on this budget +- $45/leg at $7 ≈ 6 levels/leg (thin but allowed) +- **Hard gate:** `position_mode_check` → `mode` is only **HEDGE** or **ONEWAY** +- `two_sided_allowed: NO` (ONEWAY, including when `mode_read: SHRUG` defaulted) → **omit TWO_SIDED**, favored single side +- Do **not** auto-set HEDGE unless user ordered it +- Never raise budget to fit a leg -**Layer 2 — Hourly check (4h + 1d): manages the RUNNING grid** -- Decides whether to keep, replace, or stop the running grid -- Direction change requires BOTH 4h and 1d to confirm the new direction +## Layer map -## Each Tick (every ~1 hour) +**Layer 1 baseline (first entry / flat re-entry):** +- BULLISH incl weak → LONG +- BEARISH incl weak → SHORT +- NEUTRAL → ladder after menu: TWO_SIDED (if allowed) → best single side lean → HOLD +- Hourly never vetoes first entry (prices / ATR-D only) -### 1. Baseline check -Run `baseline_7d` if no baseline exists yet or last run was >24h ago: +**Layer 2 hourly (running grid only):** +- keep / passive / flip only if both 4h+1d opposite + age ≥3h +- Before re-opening TWO_SIDED → position_mode_check again + +## Each tick + +### 1. Baseline (if missing or >24h) ``` -manage_routines(action="run", name="baseline_7d", strategy_id="adaptive_grid_trader.sol_usdt_adaptive_grid", - config={"trading_pair": "SOL-USDT", "connector_name": "binance_perpetual"}) +manage_routines(action="run", name="baseline_7d", + strategy_id="adaptive_grid_trader.sol_usdt_adaptive_grid", + config={"trading_pair":"SOL-USDT","connector_name":"binance_perpetual"}) ``` -Store the ATR value and trend direction. -### 2. Market analysis -Run `hourly_mtf_check` to get the current profile recommendation: +### 2. Hourly MTF ``` -manage_routines(action="run", name="hourly_mtf_check", strategy_id="adaptive_grid_trader.sol_usdt_adaptive_grid", - config={"trading_pair": "SOL-USDT", "connector_name": "binance_perpetual", - "lifetime_hours": 8.0, "baseline_atr": }) +manage_routines(action="run", name="hourly_mtf_check", + strategy_id="adaptive_grid_trader.sol_usdt_adaptive_grid", + config={"trading_pair":"SOL-USDT","connector_name":"binance_perpetual", + "lifetime_hours":8.0,"baseline_atr":}) ``` -Read the recommendation: profile, confidence, start_price, end_price, limit_price, D value. +Ignore PROFILE=HOLD as first-entry veto. -### 3. Read current state -Check what's actually running — never trust stored state: +### 3. Live state ``` manage_executors(action="search", connector_names=["binance_perpetual"], - trading_pairs=["SOL-USDT"], executor_types=["grid_executor"], status="RUNNING") + trading_pairs=["SOL-USDT"], executor_types=["grid_executor"], status="RUNNING") +get_portfolio_overview(connector_names=["binance_perpetual"], + include_perp_positions=True, include_balances=True, + include_lp_positions=False, include_active_orders=True) ``` -Also check exchange position: +### 3a. Orphan cleanup (before any deploy) +If step 3 shows **active orders on SOL-USDT** but **no running executor owns them**, they are stale leftovers. +1. Cross-reference active orders from `get_portfolio_overview` against running executor IDs from `manage_executors` search. +2. Any order whose `client_order_id` does not belong to a running executor → cancel it: + ``` + manage_executors(action="cancel_order", connector_name="binance_perpetual", + trading_pair="SOL-USDT", order_id="") + ``` +3. If cancel fails, retry once. If still stuck, journal the orphan and **continue** (do not HOLD solely because of an uncancellable orphan — attempt deployment anyway unless the orphan blocks balance). +4. Verify orders are gone before proceeding to deploy. + +### 3b. Account menu (mandatory on flat / first entry / before TWO_SIDED) ``` -get_portfolio_overview(connector_names=["binance_perpetual"], - include_perp_positions=True, include_balances=True, - include_lp_positions=False, include_active_orders=False) +manage_routines(action="run", name="position_mode_check", + strategy_id="adaptive_grid_trader", + config={"connector_name":"binance_perpetual","account_name":"master_account"}) ``` +Branch only on **`mode: HEDGE|ONEWAY`** and **`two_sided_allowed`**. +Optional `mode_read: SHRUG (...)` = unreadable path already folded into ONEWAY — one-sided path only. ### 4. Decide +- A flat: baseline direction or NEUTRAL ladder (menu from 3b) +- B grid died: clean → 4h+1d agree one-sided else Case A +- C/D running keep if same/neutral/disagree +- E flip if both opposite + ≥3h +- F TWO_SIDED running + TF lock → both down → one-sided -**Case A — No grid running, first entry:** -- Use the BASELINE trend to decide direction: - - Baseline BULLISH → deploy LONG grid (go to step 6) - - Baseline BEARISH → deploy SHORT grid (go to step 6) - - Baseline NEUTRAL → HOLD, wait for trend to emerge +### 5. Teardown +stop keep_position=False; both legs if two-sided; verify flat; notify if orphan stuck. -**Case B — No grid running, grid died on its own (limit_price hit, time_limit, or FAILED):** -- First check hourly signal: if both 4h + 1d confirm a direction → deploy in that direction -- If hourly is NEUTRAL/disagree → fall back to latest baseline trend for direction -- If baseline also NEUTRAL → HOLD +### 6. Liq guard +liquidation_guard skill; $90 one-sided / $45 per leg; per_level ≥7. -**Case C — Grid IS running, hourly check says same direction:** -- No change. Journal it. +### 7. Deploy grid_executor +total_amount_quote 90 (or 45×2 if two_sided_allowed YES only); min_order 7; max_open_orders 12; activation_bounds 0.002; TP≥0.001; time_limit 43200; keep_position false; controller_id = this session agent_id. -**Case D — Grid IS running, hourly check says NEUTRAL or timeframes disagree:** -- No change. Grid keeps running passively (HOLD ≠ stop). - -**Case E — Grid IS running, hourly check says OPPOSITE direction (both 4h + 1d confirm):** -- Apply minimum lifetime: has current grid been running ≥3h? If not → no change. -- If ≥3h → proceed to teardown (step 5) then deploy (step 6). - -### 5. Teardown (when needed) -``` -manage_executors(action="stop", executor_id="", keep_position=False) -``` -Then **verify** position is flat: -``` -get_portfolio_overview(connector_names=["binance_perpetual"], - include_perp_positions=True, include_balances=False, - include_lp_positions=False, include_active_orders=False) -``` -If position ≠ 0 → orphan recovery: -1. Close with reduce-only order -2. Re-check position -3. Max 3 retries, then STOP and alert: `send_notification(text="⚠️ Adaptive Grid: orphan position on SOL-USDT binance_perpetual, manual intervention needed")` - -**Never deploy a new grid until position is verified flat.** - -### 6. Pre-deploy checks (Liquidation Guard skill) - -Read the `liquidation_guard` skill and follow all steps: - -**Step 0 — Order size**: `per_level = 90 / levels`. Must be ≥ $7. With $90 budget: max 12 levels. - -**Step 1 — Position size**: compute total_base and avg_entry assuming all levels fill. - -**Step 2 — Liquidation price**: -- LONG: `liq_price = avg_entry × (1 - 1/leverage + 0.004)` -- SHORT: `liq_price = avg_entry × (1 + 1/leverage - 0.004)` - -**Step 3 — Check**: LONG: liq_price must be < limit_price. SHORT: liq_price must be > limit_price. - -**Step 4 — If FAIL**: reduce leverage → recompute. If still fails → HOLD and journal why. - -### 7. Deploy - -Build the grid_executor config. ALL fields must be present: - -```python -executor_config = { - "connector_name": "binance_perpetual", - "trading_pair": "SOL-USDT", - "side": 1, # 1=BUY(LONG), 2=SELL(SHORT) - "start_price": , - "end_price": , - "limit_price": , - "total_amount_quote": 90, # budget minus reserve - "min_order_amount_quote": 7, - "min_spread_between_orders": , - "max_open_orders": 12, # max levels given budget - "activation_bounds": 0.002, # 0.2% - "order_frequency": 5, - "max_orders_per_batch": 1, - "keep_position": False, # ALWAYS false - "coerce_tp_to_step": True, - "triple_barrier_config": { - "take_profit": , - "open_order_type": 3, # LIMIT_MAKER - "take_profit_order_type": 3, # LIMIT_MAKER - "time_limit": 43200 # 12h dead-man's switch - } -} -``` +### 8. Journal +entry_path, mode (HEDGE|ONEWAY), mode_read if present, two_sided_allowed, baseline, 4h/1d, liq_guard. -Deploy: -``` -manage_executors(action="create", executor_type="grid_executor", executor_config=, - controller_id="adaptive_grid_trader.sol_usdt_adaptive_grid") -``` +## Constraints +- First entry baseline-driven +- mode ONEWAY or two_sided_allowed NO → never two grids +- No stop_loss / trailing_stop +- Fee-clear TP and spacing -### 8. Journal -Write every decision: -``` -trading_agent_journal_write(agent_id=, entry_type="action", - text="", reasoning="", tick=) -``` -Write learnings when you discover something new about this market. - -## Key constraints -- **min_spread_between_orders** and **take_profit** must both exceed round-trip fees. Binance perpetual maker fee is typically 0.02% (2 bps). Round-trip = 4 bps. Set take_profit ≥ 0.001 (10 bps) minimum to have margin. -- **Never set stop_loss or trailing_stop** in triple_barrier_config. -- **TWO_SIDED is disabled** for this strategy to keep it simple. -- **Leverage**: start at 3x, max 5x. Always run liquidation guard. -- With ~12 levels max, grid can be reasonably dense. - -## Reporting format -- action: no change | deploy | stop | replace | blocked -- direction: LONG | SHORT -- levels: N at $X each -- range: start → end (limit at X) -- liq_guard: PASS (liq_price: X, buffer: Y%) -- worst_case_loss: $X (Y% of budget) -- 1h: range X–Y, ATR Z, volatility HIGH/MED/LOW -- 4h/1d: BULLISH/BEARISH/NEUTRAL -- baseline: BULLISH/BEARISH/NEUTRAL (7d trend) From b3a98ea4c08f7ae178d8f090208f1aba7b7a7156 Mon Sep 17 00:00:00 2001 From: rapcmia Date: Fri, 31 Jul 2026 14:34:45 +0800 Subject: [PATCH 4/6] fix(agents): daily timeframe always read NEUTRAL, blocking every flip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _trend_direction bails out below slow + 2 = 23 candles, but the routine fetched only 20 daily candles. The daily timeframe therefore returned NEUTRAL unconditionally — proven with a synthetic 2%-per-candle uptrend, which reads NEUTRAL at 20 candles and BULLISH at 23. Because a direction change requires both 4h and 1d to agree, flips were unreachable: cases E and F never fired, case B always fell through to the baseline, and confidence could never exceed LOW. - fetch 60 candles for both 4h and 1d, with the 23-candle floor documented - drop stale "proxy for 6h / 12h" comments left from the previous design - add triple_barrier stop_loss (0.10) so a losing grid closes on PnL instead of waiting for limit_price or the time limit; note that it measures filled-position PnL, not budget Co-Authored-By: Claude Opus 5 (1M context) --- agents/adaptive_grid_trader/AGENT.md | 2 +- .../adaptive_grid_trader/routines/hourly_mtf_check.py | 10 ++++++---- .../strategies/btc_usdt_adaptive_grid/strategy.md | 4 ++-- .../strategies/sol_usdt_adaptive_grid/strategy.md | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/agents/adaptive_grid_trader/AGENT.md b/agents/adaptive_grid_trader/AGENT.md index 9412ebb6..aaa8aae9 100644 --- a/agents/adaptive_grid_trader/AGENT.md +++ b/agents/adaptive_grid_trader/AGENT.md @@ -170,7 +170,7 @@ If hourly HOLD but Layer 1 deploys → build prices from ATR/D yourself. No fixe **Liq guard** before every deploy (full fill worst case): LONG liq < limit; SHORT liq > limit. Else reduce leverage / narrow / HOLD. -**Exit:** limit_price only — no stop_loss / trailing_stop in triple_barrier. +**Exit:** limit_price + stop_loss. `stop_loss` is a % of the **filled** position's PnL (not of budget) and is checked before limit_price, so it bites harder early in a grid's life than at full fill. Leave `stop_loss_order_type` at MARKET — the executor rejects anything else. Still no trailing_stop. Set time_limit dead-man switch. Normal stop + verify flat. Orphan = reduce-only close, retry bound, alert, never stack grids on dirt. diff --git a/agents/adaptive_grid_trader/routines/hourly_mtf_check.py b/agents/adaptive_grid_trader/routines/hourly_mtf_check.py index 1551fda5..7d72c43c 100644 --- a/agents/adaptive_grid_trader/routines/hourly_mtf_check.py +++ b/agents/adaptive_grid_trader/routines/hourly_mtf_check.py @@ -133,8 +133,10 @@ async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: try: raw_1h, raw_4h, raw_1d = await asyncio.gather( client.market_data.get_candles(config.connector_name, config.trading_pair, "1h", max_records=50), - client.market_data.get_candles(config.connector_name, config.trading_pair, "4h", max_records=30), - client.market_data.get_candles(config.connector_name, config.trading_pair, "1d", max_records=20), + # _trend_direction needs slow EMA period + 2 = 23 candles minimum, + # so keep a margin above that or the timeframe reads NEUTRAL forever. + client.market_data.get_candles(config.connector_name, config.trading_pair, "4h", max_records=60), + client.market_data.get_candles(config.connector_name, config.trading_pair, "1d", max_records=60), ) except Exception as e: logger.error(f"[hourly_mtf_check] candle fetch failed: {e}") @@ -157,10 +159,10 @@ async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: vol_level, range_high, range_low, range_pct, range_pos = _volatility_level(atr_1h, window_24) current_price = float(candles_1h[-1].get("close", 0) or 0) - # 4h (proxy for 6h): trend direction + # 4h: trend direction trend_4h = _trend_direction(candles_4h) if candles_4h else "NEUTRAL" - # 1d (proxy for 12h): trend confirmation + # 1d: trend confirmation trend_1d = _trend_direction(candles_1d) if candles_1d else "NEUTRAL" # -- 3. Signal synthesis -- diff --git a/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md b/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md index f177d5d9..5393e257 100644 --- a/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md +++ b/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md @@ -103,7 +103,7 @@ Envelope already forbids TWO_SIDED; even if HEDGE/two_sided YES, **still one gri ### 5–7. Teardown / liq guard / deploy - total_amount_quote **54**, min_order **6.5**, max_open_orders **8**, activation_bounds 0.002 -- TP ≥ 0.001, keep_position false, controller_id = session agent_id +- TP ≥ 0.001, stop_loss **0.10**, keep_position false, controller_id = session agent_id - BTC-USDT only ### 8. Journal @@ -112,6 +112,6 @@ entry_path, mode (HEDGE|ONEWAY), mode_read if any, two_sided_allowed, baseline, ## Constraints - First entry baseline-driven - TWO_SIDED disabled regardless of HEDGE -- No stop_loss / trailing_stop +- stop_loss 0.10 = 10% of **filled** position PnL, not of budget — tighter in dollars early in the grid's life. No trailing_stop. - Fee-clear spacing/TP diff --git a/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md b/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md index 4ebf93c1..bb6237c4 100644 --- a/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md +++ b/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md @@ -116,7 +116,7 @@ stop keep_position=False; both legs if two-sided; verify flat; notify if orphan liquidation_guard skill; $90 one-sided / $45 per leg; per_level ≥7. ### 7. Deploy grid_executor -total_amount_quote 90 (or 45×2 if two_sided_allowed YES only); min_order 7; max_open_orders 12; activation_bounds 0.002; TP≥0.001; time_limit 43200; keep_position false; controller_id = this session agent_id. +total_amount_quote 90 (or 45×2 if two_sided_allowed YES only); min_order 7; max_open_orders 12; activation_bounds 0.002; TP≥0.001; stop_loss 0.10; time_limit 43200; keep_position false; controller_id = this session agent_id. ### 8. Journal entry_path, mode (HEDGE|ONEWAY), mode_read if present, two_sided_allowed, baseline, 4h/1d, liq_guard. @@ -124,6 +124,6 @@ entry_path, mode (HEDGE|ONEWAY), mode_read if present, two_sided_allowed, baseli ## Constraints - First entry baseline-driven - mode ONEWAY or two_sided_allowed NO → never two grids -- No stop_loss / trailing_stop +- stop_loss 0.10 = 10% of **filled** position PnL, not of budget — tighter in dollars early in the grid's life. No trailing_stop. - Fee-clear TP and spacing From 9c472e3c681d17ab5f1575d8849fd97c12db2465 Mon Sep 17 00:00:00 2001 From: rapcmia Date: Mon, 3 Aug 2026 14:26:09 +0800 Subject: [PATCH 5/6] feat(agents): react to grid results, and commit to a direction sooner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The baseline sat on NEUTRAL through obvious moves because it only looked at EMA separation, and a running grid had no feedback path at all — it was kept or flipped purely on chart agreement. baseline_7d: - EMA separation threshold 0.3 -> 0.15 ATR - add a price-action override: price on one side of both EMAs with a 48h slope past +/-1% reads directional even while EMAs are still crossing - add a soft nudge at +/-0.25% for the remaining NEUTRAL cases - retune strength bands to 0.15 / 0.8, and report price-vs-EMAs and slope Layer 2, checked before keep/flip in priority order: - stale grids that stopped filling are torn down and rebuilt on a fresh range - grids at >=2% unrealized profit of trade budget are banked - sustained worsening PnL counts as a confirming vote, substituting for one missing timeframe; it never overrides a clear opposite baseline Co-Authored-By: Claude Opus 5 (1M context) --- agents/adaptive_grid_trader/AGENT.md | 74 ++++++++++++++- .../routines/baseline_7d.py | 85 +++++++++++++++--- .../btc_usdt_adaptive_grid/strategy.md | 84 +++++++++++++++-- .../sol_usdt_adaptive_grid/strategy.md | 89 +++++++++++++++++-- 4 files changed, 305 insertions(+), 27 deletions(-) diff --git a/agents/adaptive_grid_trader/AGENT.md b/agents/adaptive_grid_trader/AGENT.md index aaa8aae9..41aeadb7 100644 --- a/agents/adaptive_grid_trader/AGENT.md +++ b/agents/adaptive_grid_trader/AGENT.md @@ -33,6 +33,9 @@ You are an expert in **adaptive grid trading** — deploying directional grids ( - **Grid construction**: use the allocated budget to work out how many valid orders fit. LONG or SHORT may use the full allocation; TWO_SIDED splits it 50/50 between the two legs. Build the result as a `grid_executor` payload. - **Risk management**: set leverage from market risk and account size (1x spot, 3x–10x perps). Set `limit_price` as the grid invalidation price. `keep_position` is always `False`. Set `triple_barrier_config.take_profit` for the per-level profit target. - **Position verification**: cancel all orders, close the position with reduce-only, verify position = 0, retry within limits, alert if anything remains +- **PnL feedback**: track running grid PnL across ticks and use worsening losses as a confirming signal to break NEUTRAL deadlocks +- **Stale grid recycling**: detect grids past `time_limit` with no new fills for 3+ ticks and redeploy with fresh range +- **Profit-taking**: close grids at ≥2% unrealized profit of trade budget, realize gains, and redeploy if signals confirm ## What you do NOT handle @@ -144,6 +147,68 @@ Optional flavor line (never a third branch): **Key rules:** anti-flip needs both 4h+1d; min lifetime ~3h; emergency exits exempt. +**PnL-Aware Signal Adjustment (Layer 2 enhancement):** + +Running grids produce real market feedback via their PnL. Use this to break NEUTRAL deadlocks and accelerate direction changes. + +**How it works:** +1. **Track PnL trend** — each tick, record the grid's unrealized PnL. Track direction (improving/worsening) over the last 3+ ticks. +2. **PnL confirms direction change** — if ALL of these are true, the PnL signal fires: + - Current grid PnL is **negative** + - PnL has been **worsening** (becoming more negative) over **3+ consecutive ticks** + - The grid is on the **wrong side** (e.g., LONG grid with worsening losses = market moving against it) +3. **How PnL modifies decisions:** + +| Baseline | 4h | 1d | PnL signal | Action | +|----------|----|----|------------|--------| +| NEUTRAL | NEUTRAL | NEUTRAL | Worsening LONG losses | → treat as BEARISH baseline, teardown + SHORT | +| NEUTRAL | BEARISH | NEUTRAL | Worsening LONG losses | → PnL confirms 4h, teardown + SHORT (don't need both 4h+1d) | +| NEUTRAL | NEUTRAL | BEARISH | Worsening LONG losses | → PnL confirms 1d, teardown + SHORT (don't need both 4h+1d) | +| BEARISH | NEUTRAL | NEUTRAL | Worsening LONG losses | → baseline + PnL agree, teardown + SHORT | +| BULLISH | any | any | Worsening LONG losses | → PnL does NOT override a clear opposite baseline. Keep grid. | + +**The rule:** PnL breaks NEUTRAL deadlocks but never overrides a clear directional baseline. It acts as a confirming vote that substitutes for one missing timeframe agreement. + +4. **PnL signal does NOT fire** if: + - PnL is positive (grid is working) + - PnL is negative but stable/improving (market may be turning) + - Grid has been running < 3 ticks (insufficient data) + - Grid is within normal stop_loss tolerance (expected drawdown) + +5. **Minimum lifetime still applies** — PnL-driven teardown still respects the ~3h minimum unless the loss exceeds `max_loss_pct × 0.5` (halfway to max acceptable loss), in which case it's an early exit. + +6. **Journal the PnL signal** when it fires: + ``` + pnl_signal: BEARISH (LONG grid, PnL worsening 4 ticks: -$0.12 → -$0.37) + action: teardown + SHORT (PnL confirmed 4h BEARISH, broke NEUTRAL deadlock) + ``` + +**Stale Grid Detection (Layer 2 — checked BEFORE keep/flip):** + +A grid past its `time_limit` that has stopped filling is dead weight. Detect and recycle it. + +**Stale = ALL true:** (1) age > `time_limit`, (2) `filled_amount_quote` unchanged for **3+ consecutive ticks**, (3) grid still has active orders. + +**Action:** teardown (keep_position=False, verify flat) → re-run baseline if >6h old → redeploy fresh range on current price via ATR/D. Same direction is fine if baseline still agrees; if baseline flipped, use new direction. For TWO_SIDED: check each leg independently. + +Journal: `stale_recycle: true, ticks_stagnant: N, old_volume: $X` + +**Profit-Taking Rule (Layer 2 — checked BEFORE keep/flip):** + +Lock in meaningful unrealized profit instead of riding it back to zero. + +**Threshold:** unrealized PnL (`net_pnl_quote`) ≥ **2% of trade budget** (per-leg for TWO_SIDED). + +**Action:** teardown (realizes profit) → re-run hourly MTF for fresh range → if baseline+hourly confirm same direction, redeploy immediately; else follow normal Layer 1/2 flow. No minimum age for profit-taking. Does not count as a "flip" for the 3h cooldown. + +Journal: `profit_take: true, pnl_realized: $X, pct_of_budget: Y%` + +**Step 4 priority (running grids, first match wins):** +1. Stale? → teardown + redeploy +2. Profit threshold? → teardown + realize + redeploy +3. PnL flip? → teardown + flip +4. Standard Layer 2: keep / flip if both 4h+1d opposite + ≥3h + **Grid died on its own (flat re-entry):** - clean orphans first - both 4h+1d agree → one-sided that way @@ -178,12 +243,15 @@ Normal stop + verify flat. Orphan = reduce-only close, retry bound, alert, never - action first: no change | deploy | stop | replace | blocked - key: value lines -- on deploy include entry_path, mode (HEDGE|ONEWAY), two_sided_allowed, optional mode_read if SHRUG-defaulted, liq_guard, worst_case_loss, baseline, 4h/1d, exchange position +- on deploy include entry_path, mode (HEDGE|ONEWAY), two_sided_allowed, optional mode_read if SHRUG-defaulted, liq_guard, worst_case_loss, baseline, 4h/1d, exchange position, pnl_signal (if active) - if mode_read SHRUG present: journal `mode: ONEWAY | mode_read: SHRUG (defaulted) | two_sided_allowed: NO` +- if PnL signal fired: include `pnl_signal: ()` +- if stale recycled: include `stale_recycle: true, ticks_stagnant: N` +- if profit taken: include `profit_take: true, pnl_realized: $X` +- always journal `filled_amount_quote` and `net_pnl_quote` every tick for trend tracking ### Routines -- `baseline_7d` — market compass +- `baseline_7d` — market compass (reports trend direction, strength, price-vs-EMAs, 48h price slope) - `hourly_mtf_check` — prices + Layer-2; never first-entry veto - `position_mode_check` — **mode is only HEDGE or ONEWAY**; unreadable path already defaulted to ONEWAY with optional `mode_read: SHRUG`. Act on `two_sided_allowed`. Look-only. - diff --git a/agents/adaptive_grid_trader/routines/baseline_7d.py b/agents/adaptive_grid_trader/routines/baseline_7d.py index 9862afb0..c6316712 100644 --- a/agents/adaptive_grid_trader/routines/baseline_7d.py +++ b/agents/adaptive_grid_trader/routines/baseline_7d.py @@ -41,6 +41,17 @@ def _compute_atr(candles: list, period: int) -> float: return sum(recent_trs) / len(recent_trs) +def _price_slope_pct(closes: list, lookback: int = 48) -> float: + """Return % change of close over the last `lookback` candles (or all available). + Positive = price rising, negative = price falling.""" + n = min(lookback, len(closes)) + if n < 2: + return 0.0 + start = closes[-n] + end = closes[-1] + return ((end - start) / start * 100) if start else 0.0 + + async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: client = await get_client(context._chat_id, context=context) if not client: @@ -96,24 +107,73 @@ async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: trend_strength = "weak" ema20_val = ema50_val = 0.0 sep_vs_atr = 0.0 + price_slope = 0.0 + price_vs_emas = "—" else: ema20_val = ema20_series[-1] ema50_val = ema50_series[-1] separation = ema20_val - ema50_val sep_vs_atr = abs(separation) / atr if atr > 0 else 0.0 + # --- Price slope over last 48h (2 days) --- + price_slope = _price_slope_pct(closes, 48) + + # --- Price position relative to EMAs --- + price_below_both = current_price < ema20_val and current_price < ema50_val + price_above_both = current_price > ema20_val and current_price > ema50_val + if price_below_both: + price_vs_emas = "BELOW_BOTH" + elif price_above_both: + price_vs_emas = "ABOVE_BOTH" + else: + price_vs_emas = "BETWEEN" + + # --- Price-action override (checked first) --- + # Strong price slope + price on one side of both EMAs = directional + # regardless of EMA ordering. This catches trends early during EMA + # crossovers where lagging EMAs would say NEUTRAL. + price_action_override = None + if price_below_both and price_slope < -1.0: + price_action_override = "BEARISH" + elif price_above_both and price_slope > 1.0: + price_action_override = "BULLISH" + + # --- Trend direction --- + # EMA separation threshold 0.15x ATR (was 0.3x): calls directional + # sooner so the baseline doesn't lag behind obvious moves. + # Micro-slope uses 6-candle lookback (was 3) for stability. if ema20_val > ema50_val: - rising = len(ema20_series) >= 3 and ema20_series[-1] > ema20_series[-3] - trend_direction = "BULLISH" if rising else "NEUTRAL" + if sep_vs_atr >= 0.15: + trend_direction = "BULLISH" + else: + rising = len(ema20_series) >= 6 and ema20_series[-1] > ema20_series[-6] + trend_direction = "BULLISH" if rising else "NEUTRAL" elif ema20_val < ema50_val: - falling = len(ema20_series) >= 3 and ema20_series[-1] < ema20_series[-3] - trend_direction = "BEARISH" if falling else "NEUTRAL" + if sep_vs_atr >= 0.15: + trend_direction = "BEARISH" + else: + falling = len(ema20_series) >= 6 and ema20_series[-1] < ema20_series[-6] + trend_direction = "BEARISH" if falling else "NEUTRAL" else: trend_direction = "NEUTRAL" - if sep_vs_atr < 0.5: + # Apply price-action override if EMA-based logic said NEUTRAL + if trend_direction == "NEUTRAL" and price_action_override: + trend_direction = price_action_override + + # --- Soft price-vs-EMA nudge for remaining NEUTRAL --- + # Lower threshold (0.25% slope, was 0.5%) so mild downtrends + # with price below both EMAs still register as directional. + if trend_direction == "NEUTRAL": + if price_below_both and price_slope < -0.25: + trend_direction = "BEARISH" + elif price_above_both and price_slope > 0.25: + trend_direction = "BULLISH" + + # --- Strength --- + if sep_vs_atr < 0.15: trend_strength = "weak" - elif sep_vs_atr < 1.5: + elif sep_vs_atr < 0.8: trend_strength = "moderate" else: trend_strength = "strong" @@ -129,7 +189,7 @@ async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: builder.section( "7-Day Market Snapshot", - f"{config.trading_pair} on {config.connector_name} | {len(records)} × 1h candles", + f"{config.trading_pair} on {config.connector_name} | {len(records)} x 1h candles", ) builder.kpi("Current Price", f"${current_price:,.2f}") builder.kpi("7D High", f"${seven_d_high:,.2f}") @@ -138,15 +198,17 @@ async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: builder.section( "Volatility — ATR", - f"Average True Range over last {config.atr_period} × 1h candles", + f"Average True Range over last {config.atr_period} x 1h candles", ) builder.kpi("ATR (1h)", f"${atr:,.2f}") builder.kpi("ATR % of Price", f"{atr_pct:.3f}%") - builder.section("Trend", "EMA20 vs EMA50 on 1h closes") + builder.section("Trend", "EMA20 vs EMA50 on 1h closes + price slope") builder.kpi("EMA20", f"${ema20_val:,.2f}") builder.kpi("EMA50", f"${ema50_val:,.2f}") - builder.kpi("EMA Sep / ATR", f"{sep_vs_atr:.2f}×") + builder.kpi("EMA Sep / ATR", f"{sep_vs_atr:.2f}x") + builder.kpi("Price vs EMAs", price_vs_emas) + builder.kpi("48h Price Slope", f"{price_slope:+.2f}%") builder.kpi("Trend Direction", trend_direction) builder.kpi("Trend Strength", trend_strength) @@ -160,7 +222,8 @@ async def run(config: Config, context: ContextTypes.DEFAULT_TYPE) -> str: f"7D Baseline — {config.trading_pair}\n" f"Price: ${current_price:,.2f} | High: ${seven_d_high:,.2f} | Low: ${seven_d_low:,.2f}\n" f"Range: {range_pct:.2f}% | ATR(1h,{config.atr_period}): ${atr:,.2f} ({atr_pct:.3f}%)\n" - f"Trend: {trend_direction} / {trend_strength} | EMA20: ${ema20_val:,.2f} | EMA50: ${ema50_val:,.2f}" + f"Trend: {trend_direction} / {trend_strength} | EMA20: ${ema20_val:,.2f} | EMA50: ${ema50_val:,.2f}\n" + f"Price vs EMAs: {price_vs_emas} | 48h Slope: {price_slope:+.2f}%" ) if report_id: summary += f"\nReport: {report_id}" diff --git a/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md b/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md index 5393e257..bd5f0588 100644 --- a/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md +++ b/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md @@ -48,6 +48,73 @@ Follow the **Agent brain** exactly. This file is envelope + tick checklist only. **Layer 2 hourly (running only):** keep / passive / flip if both 4h+1d opposite + age ≥3h +## PnL-Aware Signal Adjustment (Layer 2 modifier) + +The running grid's PnL is real market feedback. Use it as a **confirming signal** to break ties and accelerate flips when the baseline is ambiguous. + +**How to track:** Each tick, read the executor's `net_pnl_quote` from the live state (step 3). Journal the value. After 2+ ticks you have a PnL trend. + +**PnL modifier rules (applied during step 4, running grid only):** + +1. **PnL confirms direction problem (flip accelerator):** + If the grid is LONG and PnL is negative AND worsening (current PnL < previous tick PnL) for **2 consecutive ticks**, AND at least ONE of 4h/1d reads opposite (not both required): + → Treat as flip signal. Teardown the LONG grid and redeploy SHORT (if age ≥ 3h). + Same logic mirrors for SHORT grids with positive price momentum. + +2. **PnL + NEUTRAL baseline = directional push:** + If baseline is NEUTRAL and the running grid has been **negative PnL for 3+ consecutive ticks**: + → The current direction is wrong. Tear down and redeploy in the opposite direction. + Do not wait for both 4h+1d to agree — sustained negative PnL across 3 hourly ticks IS the confirmation. + +3. **PnL healthy = stronger hold:** + If PnL is positive or improving, raise the bar for flipping: require both 4h+1d opposite (standard Layer 2 rule). Do not flip a profitable grid on a single TF signal. + +**Constraints:** +- PnL modifier never overrides emergency exits (stop_loss, liq guard) +- Minimum grid age 3h still applies to PnL-triggered flips +- Journal every PnL-triggered decision with: `pnl_flip: true, pnl_trend: [values], trigger: ` + +## Stale Grid Detection (Layer 2 — step 4 check) + +A grid that has outlived its `time_limit` AND stopped filling orders is dead weight occupying budget. Detect and recycle it. + +**Definition of stale:** ALL of these must be true: +1. Grid age > `time_limit` (43200s / 12h for this envelope) +2. Executor `filled_amount_quote` (or volume) has been **unchanged for 3+ consecutive ticks** +3. Grid still has active open orders (it didn't naturally close) + +**Action when stale detected:** +1. Teardown the grid (stop, keep_position=False, verify flat) +2. Re-run baseline check (step 1) if older than 6h +3. Redeploy with fresh range centered on **current price** using standard ATR/D math +4. Journal: `stale_recycle: true, ticks_stagnant: N, old_volume: $X, reason: "past time_limit + no fills"` + +**Key rules:** +- Stale detection does NOT require a direction change — same direction redeploy is fine if baseline still agrees +- Stale check runs BEFORE the keep/flip decision (step 4) — a stale grid is never "kept" +- If baseline has flipped during staleness, the fresh deploy uses the new direction +- Volume tracking: journal `filled_amount_quote` every tick; compare current vs tick N-3 + +## Profit-Taking Rule (Layer 2 — step 4 check) + +A grid that reaches meaningful unrealized profit should lock it in rather than riding it back to zero. + +**Profit threshold:** unrealized PnL ≥ **2% of trade budget** ($1.08 on $54 trade budget) + +**Action when threshold hit:** +1. Teardown the grid (stop, keep_position=False, verify flat) — this realizes the profit +2. Journal: `profit_take: true, pnl_realized: $X, pct_of_budget: Y%` +3. Re-run hourly MTF (step 2) for fresh range prices +4. If baseline + hourly still confirm same direction → redeploy immediately with fresh range +5. If signals are mixed/opposite → follow normal Layer 1/2 decision flow (may flip or HOLD) + +**Key rules:** +- Profit-take is checked BEFORE keep/flip decision — a grid at profit threshold is always closed first +- No minimum age requirement for profit-taking (profit is profit) +- The threshold is on **unrealized PnL** (`net_pnl_quote`), not on realized fills +- After taking profit, the next grid starts fresh — no carry-over of the old range +- Profit-taking does NOT count as a "flip" for the 3h cooldown — if you take profit on a SHORT and redeploy SHORT, the new grid's flip timer starts fresh + ## Each tick ### 1. Baseline (if missing or >24h) @@ -73,6 +140,7 @@ get_portfolio_overview(connector_names=["bitget_perpetual"], include_perp_positions=True, include_balances=True, include_lp_positions=False, include_active_orders=True) ``` +**Record `net_pnl_quote` from executor search results. Compare against previous tick's journal entry to determine PnL trend.** ### 3a. Orphan cleanup (before any deploy) If step 3 shows **active orders on BTC-USDT** but **no running executor owns them**, they are stale leftovers. @@ -91,15 +159,20 @@ manage_routines(action="run", name="position_mode_check", strategy_id="adaptive_grid_trader", config={"connector_name":"bitget_perpetual","account_name":"master_account"}) ``` -Branch only on **`mode: HEDGE|ONEWAY`** + **`two_sided_allowed`**. -Envelope already forbids TWO_SIDED; even if HEDGE/two_sided YES, **still one grid only** on this budget. +Branch only on **`mode: HEDGE|ONEWAY`** + **`two_sided_allowed`**. +Envelope already forbids TWO_SIDED; even if HEDGE/two_sided YES, **still one grid only** on this budget. `mode_read: SHRUG` if present = already defaulted to ONEWAY — single-side lean path. ### 4. Decide +**Priority order for running grids (check top-down, first match wins):** +1. **Stale?** age > time_limit AND filled_amount unchanged 3+ ticks → teardown + redeploy (see Stale Grid Detection) +2. **Profit threshold?** net_pnl_quote ≥ 2% of trade budget ($1.08) → teardown + realize + redeploy (see Profit-Taking Rule) +3. **PnL flip?** rules 1/2 from PnL modifier → teardown + flip +4. **Standard Layer 2:** keep / flip if both 4h+1d opposite + ≥3h + +**Flat entry (no running grid):** - A flat: baseline LONG/SHORT or NEUTRAL lean; no hourly veto - B died: clean → 4h+1d agree else Case A -- C/D keep -- E flip if both opposite + ≥3h ### 5–7. Teardown / liq guard / deploy - total_amount_quote **54**, min_order **6.5**, max_open_orders **8**, activation_bounds 0.002 @@ -107,11 +180,10 @@ Envelope already forbids TWO_SIDED; even if HEDGE/two_sided YES, **still one gri - BTC-USDT only ### 8. Journal -entry_path, mode (HEDGE|ONEWAY), mode_read if any, two_sided_allowed, baseline, min_order 6.5 +entry_path, mode (HEDGE|ONEWAY), mode_read if any, two_sided_allowed, baseline, min_order 6.5, **net_pnl_quote, pnl_trend, filled_amount_quote** (always), **pnl_flip / stale_recycle / profit_take** (if triggered) ## Constraints - First entry baseline-driven - TWO_SIDED disabled regardless of HEDGE - stop_loss 0.10 = 10% of **filled** position PnL, not of budget — tighter in dollars early in the grid's life. No trailing_stop. - Fee-clear spacing/TP - diff --git a/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md b/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md index bb6237c4..d8b88109 100644 --- a/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md +++ b/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md @@ -56,6 +56,75 @@ Follow the **Agent brain** exactly. This file is envelope + tick checklist only. - keep / passive / flip only if both 4h+1d opposite + age ≥3h - Before re-opening TWO_SIDED → position_mode_check again +## PnL-Aware Signal Adjustment (Layer 2 modifier) + +The running grid's PnL is real market feedback. Use it as a **confirming signal** to break ties and accelerate flips when the baseline is ambiguous. + +**How to track:** Each tick, read the executor's `net_pnl_quote` from the live state (step 3). Journal the value. After 2+ ticks you have a PnL trend. + +**PnL modifier rules (applied during step 4, running grid only):** + +1. **PnL confirms direction problem (flip accelerator):** + If the grid is LONG and PnL is negative AND worsening (current PnL < previous tick PnL) for **2 consecutive ticks**, AND at least ONE of 4h/1d reads opposite (not both required): + → Treat as flip signal. Teardown the LONG grid and redeploy SHORT (if age ≥ 3h). + Same logic mirrors for SHORT grids with positive price momentum. + +2. **PnL + NEUTRAL baseline = directional push:** + If baseline is NEUTRAL and the running grid has been **negative PnL for 3+ consecutive ticks**: + → The current direction is wrong. Tear down and redeploy in the opposite direction. + Do not wait for both 4h+1d to agree — sustained negative PnL across 3 hourly ticks IS the confirmation. + +3. **PnL healthy = stronger hold:** + If PnL is positive or improving, raise the bar for flipping: require both 4h+1d opposite (standard Layer 2 rule). Do not flip a profitable grid on a single TF signal. + +**Constraints:** +- PnL modifier never overrides emergency exits (stop_loss, liq guard) +- Minimum grid age 3h still applies to PnL-triggered flips +- Journal every PnL-triggered decision with: `pnl_flip: true, pnl_trend: [values], trigger: ` + +## Stale Grid Detection (Layer 2 — step 4 check) + +A grid that has outlived its `time_limit` AND stopped filling orders is dead weight occupying budget. Detect and recycle it. + +**Definition of stale:** ALL of these must be true: +1. Grid age > `time_limit` (43200s / 12h for this envelope) +2. Executor `filled_amount_quote` (or volume) has been **unchanged for 3+ consecutive ticks** +3. Grid still has active open orders (it didn't naturally close) + +**Action when stale detected:** +1. Teardown the grid (stop, keep_position=False, verify flat) +2. Re-run baseline check (step 1) if older than 6h +3. Redeploy with fresh range centered on **current price** using standard ATR/D math +4. Journal: `stale_recycle: true, ticks_stagnant: N, old_volume: $X, reason: "past time_limit + no fills"` + +**Key rules:** +- Stale detection does NOT require a direction change — same direction redeploy is fine if baseline still agrees +- Stale check runs BEFORE the keep/flip decision (step 4) — a stale grid is never "kept" +- If baseline has flipped during staleness, the fresh deploy uses the new direction +- Volume tracking: journal `filled_amount_quote` every tick; compare current vs tick N-3 +- For TWO_SIDED: check each leg independently. One stale leg → teardown + redeploy that leg only (if the other leg is healthy) + +## Profit-Taking Rule (Layer 2 — step 4 check) + +A grid that reaches meaningful unrealized profit should lock it in rather than riding it back to zero. + +**Profit threshold:** unrealized PnL ≥ **2% of trade budget** ($1.80 on $90 one-sided, or $0.90/leg on TWO_SIDED $45/leg) + +**Action when threshold hit:** +1. Teardown the grid (stop, keep_position=False, verify flat) — this realizes the profit +2. Journal: `profit_take: true, pnl_realized: $X, pct_of_budget: Y%` +3. Re-run hourly MTF (step 2) for fresh range prices +4. If baseline + hourly still confirm same direction → redeploy immediately with fresh range +5. If signals are mixed/opposite → follow normal Layer 1/2 decision flow (may flip or HOLD) + +**Key rules:** +- Profit-take is checked BEFORE keep/flip decision — a grid at profit threshold is always closed first +- No minimum age requirement for profit-taking (profit is profit) +- The threshold is on **unrealized PnL** (`net_pnl_quote`), not on realized fills +- After taking profit, the next grid starts fresh — no carry-over of the old range +- Profit-taking does NOT count as a "flip" for the 3h cooldown — if you take profit on a SHORT and redeploy SHORT, the new grid's flip timer starts fresh +- For TWO_SIDED: check each leg independently. If one leg hits threshold, take profit on that leg and redeploy it; the other leg continues + ## Each tick ### 1. Baseline (if missing or >24h) @@ -82,6 +151,8 @@ get_portfolio_overview(connector_names=["binance_perpetual"], include_perp_positions=True, include_balances=True, include_lp_positions=False, include_active_orders=True) ``` +**Record `net_pnl_quote` from executor search results. Compare against previous tick's journal entry to determine PnL trend.** + ### 3a. Orphan cleanup (before any deploy) If step 3 shows **active orders on SOL-USDT** but **no running executor owns them**, they are stale leftovers. 1. Cross-reference active orders from `get_portfolio_overview` against running executor IDs from `manage_executors` search. @@ -99,15 +170,20 @@ manage_routines(action="run", name="position_mode_check", strategy_id="adaptive_grid_trader", config={"connector_name":"binance_perpetual","account_name":"master_account"}) ``` -Branch only on **`mode: HEDGE|ONEWAY`** and **`two_sided_allowed`**. +Branch only on **`mode: HEDGE|ONEWAY`** and **`two_sided_allowed`**. Optional `mode_read: SHRUG (...)` = unreadable path already folded into ONEWAY — one-sided path only. ### 4. Decide +**Priority order for running grids (check top-down, first match wins):** +1. **Stale?** age > time_limit AND filled_amount unchanged 3+ ticks → teardown + redeploy (see Stale Grid Detection) +2. **Profit threshold?** net_pnl_quote ≥ 2% of leg budget ($1.80 one-sided / $0.90 per TWO_SIDED leg) → teardown + realize + redeploy (see Profit-Taking Rule) +3. **PnL flip?** rules 1/2 from PnL modifier → teardown + flip +4. **Standard Layer 2:** keep / flip if both 4h+1d opposite + ≥3h +5. **TWO_SIDED collapse:** both TF lock one way → teardown both → one-sided + +**Flat entry (no running grid):** - A flat: baseline direction or NEUTRAL ladder (menu from 3b) -- B grid died: clean → 4h+1d agree one-sided else Case A -- C/D running keep if same/neutral/disagree -- E flip if both opposite + ≥3h -- F TWO_SIDED running + TF lock → both down → one-sided +- B died: clean → 4h+1d agree one-sided else Case A ### 5. Teardown stop keep_position=False; both legs if two-sided; verify flat; notify if orphan stuck. @@ -119,11 +195,10 @@ liquidation_guard skill; $90 one-sided / $45 per leg; per_level ≥7. total_amount_quote 90 (or 45×2 if two_sided_allowed YES only); min_order 7; max_open_orders 12; activation_bounds 0.002; TP≥0.001; stop_loss 0.10; time_limit 43200; keep_position false; controller_id = this session agent_id. ### 8. Journal -entry_path, mode (HEDGE|ONEWAY), mode_read if present, two_sided_allowed, baseline, 4h/1d, liq_guard. +entry_path, mode (HEDGE|ONEWAY), mode_read if present, two_sided_allowed, baseline, 4h/1d, liq_guard, **net_pnl_quote, pnl_trend, filled_amount_quote** (always), **pnl_flip / stale_recycle / profit_take** (if triggered). ## Constraints - First entry baseline-driven - mode ONEWAY or two_sided_allowed NO → never two grids - stop_loss 0.10 = 10% of **filled** position PnL, not of budget — tighter in dollars early in the grid's life. No trailing_stop. - Fee-clear TP and spacing - From b9383c0476bcaedf2cdcfef97e87dfb66661af3b Mon Sep 17 00:00:00 2001 From: rapcmia Date: Mon, 3 Aug 2026 20:12:00 +0800 Subject: [PATCH 6/6] fix(agents): set grid leverage explicitly and fix stale detection - set leverage in the executor payload; omitted, it falls back to a default well above the envelope cap - leverage comes from the envelope, not agent discretion; ask the user once at session start - stale detection no longer requires a grid older than its time_limit, which the executor never allows to exist Co-Authored-By: Claude Opus 5 (1M context) --- agents/adaptive_grid_trader/AGENT.md | 12 +++++++---- .../btc_usdt_adaptive_grid/strategy.md | 20 ++++++++++++------- .../sol_usdt_adaptive_grid/strategy.md | 13 ++++++------ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/agents/adaptive_grid_trader/AGENT.md b/agents/adaptive_grid_trader/AGENT.md index 41aeadb7..e7e264b2 100644 --- a/agents/adaptive_grid_trader/AGENT.md +++ b/agents/adaptive_grid_trader/AGENT.md @@ -31,10 +31,10 @@ You are an expert in **adaptive grid trading** — deploying directional grids ( - **Account capability gate**: run `position_mode_check` before any deploy path that might consider TWO_SIDED (and on first entry / flat re-entry when building the profile menu). It only **reads** mode — never changes mode, never places orders. - **Order sizing**: hold back the reserve set in the envelope, and size every order to at least `max(min_order_size, exchange_minimum)` - **Grid construction**: use the allocated budget to work out how many valid orders fit. LONG or SHORT may use the full allocation; TWO_SIDED splits it 50/50 between the two legs. Build the result as a `grid_executor` payload. -- **Risk management**: set leverage from market risk and account size (1x spot, 3x–10x perps). Set `limit_price` as the grid invalidation price. `keep_position` is always `False`. Set `triple_barrier_config.take_profit` for the per-level profit target. +- **Risk management**: set leverage up to the strategy's `max_leverage` (1x spot; perps capped by the envelope — never exceed it). Set `limit_price` as the grid invalidation price. `keep_position` is always `False`. Set `triple_barrier_config.take_profit` for the per-level profit target. - **Position verification**: cancel all orders, close the position with reduce-only, verify position = 0, retry within limits, alert if anything remains - **PnL feedback**: track running grid PnL across ticks and use worsening losses as a confirming signal to break NEUTRAL deadlocks -- **Stale grid recycling**: detect grids past `time_limit` with no new fills for 3+ ticks and redeploy with fresh range +- **Stale grid recycling**: detect grids with no new fills for 3+ ticks and redeploy with fresh range - **Profit-taking**: close grids at ≥2% unrealized profit of trade budget, realize gains, and redeploy if signals confirm ## What you do NOT handle @@ -60,6 +60,8 @@ The user approves these **once**, at setup. After that you run on your own and * If any of these is missing, ask once at setup. Then stop asking. +**Pre-launch leverage confirmation:** before starting a new agent session, inform the user that leverage defaults to **5x** and ask if they want a different value. This is a one-time setup question — not repeated per tick or per trade. + ## How autonomy works - **Inside the envelope → act.** Deploy, stop, or replace without asking. @@ -76,6 +78,8 @@ If any of these is missing, ask once at setup. Then stop asking. 5. Every order ≥ `max(min_order_size, exchange_minimum)` 6. **Any check fails → HOLD and report.** Never raise the budget to make a grid fit. +**Note:** leverage is set via the `leverage` field in the `grid_executor` config payload (defaults to 10x if omitted — always include it explicitly). + ### Account profile menu — `position_mode_check` (guard rail) **When to run (mandatory):** @@ -185,9 +189,9 @@ Running grids produce real market feedback via their PnL. Use this to break NEUT **Stale Grid Detection (Layer 2 — checked BEFORE keep/flip):** -A grid past its `time_limit` that has stopped filling is dead weight. Detect and recycle it. +A grid that has stopped filling is dead weight. Detect and recycle it regardless of age. -**Stale = ALL true:** (1) age > `time_limit`, (2) `filled_amount_quote` unchanged for **3+ consecutive ticks**, (3) grid still has active orders. +**Stale = ALL true:** (1) `filled_amount_quote` unchanged for **3+ consecutive ticks**, (2) grid still has active orders. **Action:** teardown (keep_position=False, verify flat) → re-run baseline if >6h old → redeploy fresh range on current price via ATR/D. Same direction is fine if baseline still agrees; if baseline flipped, use new direction. For TWO_SIDED: check each leg independently. diff --git a/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md b/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md index bd5f0588..6aa072a3 100644 --- a/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md +++ b/agents/adaptive_grid_trader/strategies/btc_usdt_adaptive_grid/strategy.md @@ -76,18 +76,17 @@ The running grid's PnL is real market feedback. Use it as a **confirming signal* ## Stale Grid Detection (Layer 2 — step 4 check) -A grid that has outlived its `time_limit` AND stopped filling orders is dead weight occupying budget. Detect and recycle it. +A grid that has stopped filling orders is dead weight occupying budget. Detect and recycle it regardless of age. **Definition of stale:** ALL of these must be true: -1. Grid age > `time_limit` (43200s / 12h for this envelope) -2. Executor `filled_amount_quote` (or volume) has been **unchanged for 3+ consecutive ticks** -3. Grid still has active open orders (it didn't naturally close) +1. Executor `filled_amount_quote` (or volume) has been **unchanged for 3+ consecutive ticks** +2. Grid still has active open orders (it didn't naturally close) **Action when stale detected:** 1. Teardown the grid (stop, keep_position=False, verify flat) 2. Re-run baseline check (step 1) if older than 6h 3. Redeploy with fresh range centered on **current price** using standard ATR/D math -4. Journal: `stale_recycle: true, ticks_stagnant: N, old_volume: $X, reason: "past time_limit + no fills"` +4. Journal: `stale_recycle: true, ticks_stagnant: N, old_volume: $X, reason: "no fills 3+ ticks"` **Key rules:** - Stale detection does NOT require a direction change — same direction redeploy is fine if baseline still agrees @@ -165,7 +164,7 @@ Envelope already forbids TWO_SIDED; even if HEDGE/two_sided YES, **still one gri ### 4. Decide **Priority order for running grids (check top-down, first match wins):** -1. **Stale?** age > time_limit AND filled_amount unchanged 3+ ticks → teardown + redeploy (see Stale Grid Detection) +1. **Stale?** filled_amount unchanged 3+ ticks → teardown + redeploy (see Stale Grid Detection) 2. **Profit threshold?** net_pnl_quote ≥ 2% of trade budget ($1.08) → teardown + realize + redeploy (see Profit-Taking Rule) 3. **PnL flip?** rules 1/2 from PnL modifier → teardown + flip 4. **Standard Layer 2:** keep / flip if both 4h+1d opposite + ≥3h @@ -174,9 +173,16 @@ Envelope already forbids TWO_SIDED; even if HEDGE/two_sided YES, **still one gri - A flat: baseline LONG/SHORT or NEUTRAL lean; no hourly veto - B died: clean → 4h+1d agree else Case A -### 5–7. Teardown / liq guard / deploy +### 5. Teardown +stop keep_position=False; verify flat; notify if orphan stuck. + +### 6. Liq guard +liquidation_guard skill; $54 budget; per_level ≥ 6.5. + +### 7. Deploy grid_executor - total_amount_quote **54**, min_order **6.5**, max_open_orders **8**, activation_bounds 0.002 - TP ≥ 0.001, stop_loss **0.10**, keep_position false, controller_id = session agent_id +- **leverage: 5** (must be included in the executor config — defaults to 10x if omitted) - BTC-USDT only ### 8. Journal diff --git a/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md b/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md index d8b88109..4bc6c311 100644 --- a/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md +++ b/agents/adaptive_grid_trader/strategies/sol_usdt_adaptive_grid/strategy.md @@ -84,18 +84,17 @@ The running grid's PnL is real market feedback. Use it as a **confirming signal* ## Stale Grid Detection (Layer 2 — step 4 check) -A grid that has outlived its `time_limit` AND stopped filling orders is dead weight occupying budget. Detect and recycle it. +A grid that has stopped filling orders is dead weight occupying budget. Detect and recycle it regardless of age. **Definition of stale:** ALL of these must be true: -1. Grid age > `time_limit` (43200s / 12h for this envelope) -2. Executor `filled_amount_quote` (or volume) has been **unchanged for 3+ consecutive ticks** -3. Grid still has active open orders (it didn't naturally close) +1. Executor `filled_amount_quote` (or volume) has been **unchanged for 3+ consecutive ticks** +2. Grid still has active open orders (it didn't naturally close) **Action when stale detected:** 1. Teardown the grid (stop, keep_position=False, verify flat) 2. Re-run baseline check (step 1) if older than 6h 3. Redeploy with fresh range centered on **current price** using standard ATR/D math -4. Journal: `stale_recycle: true, ticks_stagnant: N, old_volume: $X, reason: "past time_limit + no fills"` +4. Journal: `stale_recycle: true, ticks_stagnant: N, old_volume: $X, reason: "no fills 3+ ticks"` **Key rules:** - Stale detection does NOT require a direction change — same direction redeploy is fine if baseline still agrees @@ -175,7 +174,7 @@ Optional `mode_read: SHRUG (...)` = unreadable path already folded into ONEWAY ### 4. Decide **Priority order for running grids (check top-down, first match wins):** -1. **Stale?** age > time_limit AND filled_amount unchanged 3+ ticks → teardown + redeploy (see Stale Grid Detection) +1. **Stale?** filled_amount unchanged 3+ ticks → teardown + redeploy (see Stale Grid Detection) 2. **Profit threshold?** net_pnl_quote ≥ 2% of leg budget ($1.80 one-sided / $0.90 per TWO_SIDED leg) → teardown + realize + redeploy (see Profit-Taking Rule) 3. **PnL flip?** rules 1/2 from PnL modifier → teardown + flip 4. **Standard Layer 2:** keep / flip if both 4h+1d opposite + ≥3h @@ -192,7 +191,7 @@ stop keep_position=False; both legs if two-sided; verify flat; notify if orphan liquidation_guard skill; $90 one-sided / $45 per leg; per_level ≥7. ### 7. Deploy grid_executor -total_amount_quote 90 (or 45×2 if two_sided_allowed YES only); min_order 7; max_open_orders 12; activation_bounds 0.002; TP≥0.001; stop_loss 0.10; time_limit 43200; keep_position false; controller_id = this session agent_id. +total_amount_quote 90 (or 45×2 if two_sided_allowed YES only); min_order 7; max_open_orders 12; activation_bounds 0.002; TP≥0.001; stop_loss 0.10; time_limit 43200; keep_position false; **leverage: 5** (must be in executor config — defaults to 10x if omitted); controller_id = this session agent_id. ### 8. Journal entry_path, mode (HEDGE|ONEWAY), mode_read if present, two_sided_allowed, baseline, 4h/1d, liq_guard, **net_pnl_quote, pnl_trend, filled_amount_quote** (always), **pnl_flip / stale_recycle / profit_take** (if triggered).