Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
223f793
feat(executors): surface orphaned LP positions to agents β€” tick-promp…
fengtality Aug 13, 2026
06adc22
fix(mcp): resolve_orphan without executor_id returns a required-input…
fengtality Aug 13, 2026
d8fa769
test(mcp): pin the resolve_orphan required-input contract
fengtality Aug 13, 2026
0772a61
feat(clmm): expose bin_count on get_pool_info
fengtality Aug 13, 2026
7e622a3
chore: bump hummingbot-api-client to 1.5.8 for bin_count
fengtality Aug 13, 2026
4f1a9ad
feat(clmm): add manage_clmm so agents can recover orphaned LP positions
fengtality Aug 17, 2026
a091669
fix(clmm): point every orphan warning at manage_clmm
fengtality Aug 17, 2026
dbc9ffc
chore(deps): lock hummingbot-api-client at 1.5.8
fengtality Aug 17, 2026
8bf2905
style: black + isort the CLMM/orphan changes
fengtality Aug 17, 2026
04d1701
chore(agents): pin adaptive_grid_trader to sonnet
fengtality Aug 17, 2026
f5f25cb
feat(agents): rename Smart-Money Flow agent to Derive Options Trader
fengtality Aug 11, 2026
4df460d
fix(agents): make the Derive Options Trader routines pass #203's gates
fengtality Aug 17, 2026
3d9f2ef
test(agents): teach the numeric-credentials stub the SEC-178 access p…
fengtality Aug 18, 2026
6404c9b
chore(deps): resolve the 1.5.8 pin from the client PR branch until it…
fengtality Aug 18, 2026
73221b4
fix(agents): close four holes the adversarial review found in orphan …
fengtality Aug 18, 2026
2ad6a2b
revert(agents): drop the SYSTEM_CLEANUP tick-prompt warning
fengtality Aug 18, 2026
56f2a27
fix(clmm): position_info lists the wallet β€” pool_address was a Gatewa…
fengtality Aug 18, 2026
cb138bf
chore(deps): bump the client lock to the contract-gap commit
fengtality Aug 18, 2026
cb0695b
fix(dex): align with hapi contracts; drop pool-scoped AMM swaps from MCP
fengtality Aug 19, 2026
f5d1d70
fix(agents): gate LP writes; register the swap tool; stop forcing 1% …
fengtality Aug 19, 2026
7d194ec
fix(dex): read the fields hapi actually sends; resolve networks dynam…
fengtality Aug 19, 2026
fb38b81
fix(agents): describe LP writes in the confirmation prompt
fengtality Aug 19, 2026
8e5a22f
chore(deps): lock the client to the dead-pool-method removal
fengtality Aug 19, 2026
0b22d96
test(lp): cover the last read action, swap get_status
fengtality Aug 20, 2026
ae410a0
docs(gateway): record GW-17, the add/remove sign split
fengtality Aug 20, 2026
42c5786
docs(gateway): confirm GW-17 with the completed round trip
fengtality Aug 20, 2026
8df9d52
docs(gateway): record GW-18, pancakeswap-sol's flattened close accoun…
fengtality Aug 20, 2026
528f1ee
test(lp): exercise orca CLMM and meteora DAMM v2; record GW-19
fengtality Aug 20, 2026
f7e210f
test(lp): size the meteora legs to the fill, and read balances with r…
fengtality Aug 20, 2026
f17b0da
docs(gateway): record GW-20, position rent booked as deposited liquidity
fengtality Aug 20, 2026
ed8a147
docs(gateway): record GW-21; rename the meteora close step to drain
fengtality Aug 20, 2026
4362d6a
docs(gateway): GW-21 fixed by collapsing the routes; restore the clos…
fengtality Aug 20, 2026
5c273bc
docs(gateway): GW-20 fixed in both halves
fengtality Aug 20, 2026
51300d7
fix(clmm): read the position that was asked for
fengtality Aug 20, 2026
87a4fad
docs(gateway): record GW-22 so a parallel session does not re-find it
fengtality Aug 20, 2026
d2a1dc0
docs(gateway): record GW-23, money typed as JSON number
fengtality Aug 20, 2026
2485e00
test(orca): cover all three range shapes, with ranges derived from spot
fengtality Aug 20, 2026
1529d04
docs(gateway): record GW-24, Orca's open spends the slippage bound
fengtality Aug 20, 2026
81f2c1d
docs(gateway): summarise what is fixed, spell out what is not
fengtality Aug 20, 2026
5f0dd5c
docs(gateway): record GW-25, narrow in-range closes fail on slippage
fengtality Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
561 changes: 561 additions & 0 deletions GATEWAY_ISSUES.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion agents/adaptive_grid_trader/AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: Adaptive Grid Trader
description: Expert in multi-timeframe adaptive grid trading with safety-first order
sizing, a configurable untraded reserve, and strict risk management
agent_key: claude-acp:opus
agent_key: claude-acp:sonnet
tools:
- get_market_data
- get_portfolio_overview
Expand Down
374 changes: 374 additions & 0 deletions agents/adaptive_grid_trader/routines/kalman_grid_operator.py

Large diffs are not rendered by default.

194 changes: 194 additions & 0 deletions agents/adaptive_grid_trader/routines/kalman_regime_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@

from pydantic import BaseModel, Field
from telegram.ext import ContextTypes
from config_manager import get_client
from condor.reports import ReportBuilder
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import logging
from datetime import datetime, timezone

logger = logging.getLogger(__name__)

CATEGORY = "Analysis"


def _candle_minutes(interval: str) -> float:
"""Parse '1m'β†’1, '1h'β†’60, '4h'β†’240, '1d'β†’1440."""
if interval.endswith("m"):
return float(interval[:-1])
elif interval.endswith("h"):
return float(interval[:-1]) * 60
elif interval.endswith("d"):
return float(interval[:-1]) * 1440
return 1.0


class Config(BaseModel):
"""Kalman filter regime detection β€” outputs regime signal and Kalman-sized grid params."""
connector_name: str = Field(default="bitget_perpetual", description="Exchange connector")
trading_pair: str = Field(default="BTC-USDT", description="Trading pair")
candle_interval: str = Field(default="1m", description="Candle interval (1m, 5m, 1h, etc.)")
lookback_candles: int = Field(default=60, description="Candles to fetch (60 Γ— 1m = last 1 hour)")
q_level: float = Field(default=100.0, description="Process noise: level variance")
q_slope: float = Field(default=1.0, description="Process noise: slope variance")
r_obs: float = Field(default=40.0, description="Observation noise variance")
snr_trend_threshold: float = Field(default=0.3, description="SNR >= this β†’ TRENDING")
snr_range_threshold: float = Field(default=0.1, description="SNR <= this β†’ RANGING")
target_grid_hours: float = Field(default=8.0, description="Target grid lifetime (hours) for D sizing")


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"

result = await client.market_data.get_candles(
config.connector_name, config.trading_pair,
interval=config.candle_interval, max_records=config.lookback_candles
)
records = result if isinstance(result, list) else result.get("data", result.get("candles", []))
if not records or len(records) < 20:
return f"Insufficient candle data: got {len(records) if records else 0} records"

closes, timestamps = [], []
for r in records:
try:
closes.append(float(r["close"]))
ts = r.get("timestamp", r.get("time", 0))
if isinstance(ts, (int, float)) and ts > 0:
t = ts / 1000 if ts > 1e10 else ts
timestamps.append(datetime.fromtimestamp(t, tz=timezone.utc).strftime("%m-%d %H:%M"))
else:
timestamps.append(str(ts))
except (KeyError, TypeError, ValueError):
continue

if len(closes) < 20:
return f"Could not parse enough closes: got {len(closes)}"

prices = np.array(closes)
n = len(prices)
x_axis = timestamps[:n] if len(timestamps) == n else list(range(n))

# Local Linear Trend Kalman Filter
F = np.array([[1.0, 1.0], [0.0, 1.0]])
H = np.array([[1.0, 0.0]])
Q = np.diag([config.q_level, config.q_slope])
R = np.array([[config.r_obs]])
x = np.array([prices[0], 0.0])
P = np.eye(2) * 1e6

levels, slopes, innovations = [], [], []
for price in prices:
x_pred = F @ x
P_pred = F @ P @ F.T + Q
inn = price - float((H @ x_pred)[0])
S = float((H @ P_pred @ H.T + R)[0, 0])
K = (P_pred @ H.T / S).flatten()
x = x_pred + K * inn
P = (np.eye(2) - np.outer(K, H)) @ P_pred
levels.append(float(x[0]))
slopes.append(float(x[1]))
innovations.append(float(inn))

levels = np.array(levels)
slopes = np.array(slopes)
innovations = np.array(innovations)

window = min(24, max(n // 6, 4))
recent_slope = slopes[-1]
recent_price = prices[-1]
rms_inn = float(np.sqrt(np.mean(innovations[-window:] ** 2)))
sigma_inn = max(rms_inn, 1.0)
slope_pct = recent_slope / recent_price * 100
snr = abs(recent_slope) / sigma_inn

# D: scale by candle duration so grid width is timeframe-agnostic
candle_min = _candle_minutes(config.candle_interval)
grid_D = sigma_inn * float(np.sqrt(config.target_grid_hours * 60 / candle_min))

if n < 30:
regime, profile = "UNCERTAIN", "HOLD"
elif snr >= config.snr_trend_threshold:
regime = "TRENDING_UP" if recent_slope > 0 else "TRENDING_DOWN"
profile = "LONG" if recent_slope > 0 else "SHORT"
elif snr <= config.snr_range_threshold:
regime, profile = "RANGING", "TWO_SIDED"
else:
regime, profile = "UNCERTAIN", "HOLD"

if regime == "TRENDING_UP":
g_start = recent_price - grid_D
g_end = recent_price + 3 * grid_D
g_limit = recent_price - 1.5 * grid_D
elif regime == "TRENDING_DOWN":
g_start = recent_price - 3 * grid_D
g_end = recent_price + grid_D
g_limit = recent_price + 1.5 * grid_D
else:
g_start = recent_price - 1.5 * grid_D
g_end = recent_price + 1.5 * grid_D
g_limit = None

fig = make_subplots(
rows=3, cols=1, shared_xaxes=True,
subplot_titles=[
f"Price vs Kalman Level β€” {config.trading_pair}",
"Kalman Slope ($/candle)",
"Innovation ($)"
],
row_heights=[0.5, 0.25, 0.25],
vertical_spacing=0.07,
)
fig.add_trace(go.Scatter(x=x_axis, y=prices.tolist(), name="Close",
line=dict(color="#94a3b8", width=1)), row=1, col=1)
fig.add_trace(go.Scatter(x=x_axis, y=levels.tolist(), name="Kalman Level",
line=dict(color="#60a5fa", width=2)), row=1, col=1)
slope_colors = ["#22c55e" if s > 0 else "#ef4444" for s in slopes]
fig.add_trace(go.Bar(x=x_axis, y=slopes.tolist(), name="Slope",
marker_color=slope_colors, showlegend=False), row=2, col=1)
fig.add_hline(y=0, line_dash="dash", line_color="gray", line_width=1, row=2, col=1)
inn_colors = ["#f97316" if abs(i) > 2 * sigma_inn else "#64748b" for i in innovations]
fig.add_trace(go.Bar(x=x_axis, y=innovations.tolist(), name="Innovation",
marker_color=inn_colors, showlegend=False), row=3, col=1)
fig.add_hline(y=sigma_inn, line_dash="dot", line_color="#22c55e", line_width=1, row=3, col=1)
fig.add_hline(y=-sigma_inn, line_dash="dot", line_color="#22c55e", line_width=1, row=3, col=1)
fig.update_layout(
height=700, template="plotly_dark",
title=f"Kalman Regime: {regime} | SNR={snr:.3f} | {config.candle_interval} Γ— {n}",
legend=dict(orientation="h", yanchor="top", y=-0.15, xanchor="center", x=0.5),
)

builder = ReportBuilder("Kalman Regime Check")
builder.source("routine", "kalman_regime_check")
builder.tags(["analysis", "regime", "kalman", "adaptive-grid-trader"])

builder.section("01 / REGIME SIGNAL", "Kalman filter output and profile recommendation")
builder.kpi("Regime", regime)
builder.kpi("Profile", profile)
builder.kpi("Current Price", f"${recent_price:,.2f}")
builder.kpi("Kalman Slope", f"{slope_pct:+.5f}%/candle")
builder.kpi("Innovation RMS", f"${sigma_inn:,.2f}")
builder.kpi("SNR", f"{snr:.3f}")
builder.kpi("Candles Used", str(n))

builder.section("02 / GRID SIZING", "Kalman-derived grid parameters")
builder.kpi("Grid D", f"${grid_D:,.2f}")
builder.kpi("Grid Start", f"${g_start:,.2f}")
builder.kpi("Grid End", f"${g_end:,.2f}")
builder.kpi("Limit Price", f"${g_limit:,.2f}" if g_limit else "N/A (ranging/uncertain)")

builder.section("03 / CHARTS", "Price, slope, and innovation over lookback window")
builder.plotly(fig)

builder.manual_order()
await builder.save()

return (
f"Regime: {regime} β†’ {profile}\n"
f"Price ${recent_price:,.2f} | Slope {slope_pct:+.5f}%/candle | SNR {snr:.3f}\n"
f"Grid D ${grid_D:,.2f} | Range ${g_start:,.2f}–${g_end:,.2f}"
+ (f" | Limit ${g_limit:,.2f}" if g_limit else "")
)
106 changes: 106 additions & 0 deletions agents/adaptive_grid_trader/strategies/kalman_grid_btc/strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
name: kalman_grid_btc
description: ''
agent_key: null
skills: []
default_config: {}
default_trading_context: ''
created_by: 456181693
created_at: '2026-08-13T03:14:12.657657+00:00'
---


# Kalman Grid Strategy β€” BTC-USDT / bitget_perpetual

## Envelope (fixed β€” never ask per tick)
- connector: bitget_perpetual
- pair: BTC-USDT
- budget: 100 USDT | reserve: 10% | trade_budget: $90
- max_leverage: 5x | max_loss_pct: 10% ($10 max per grid)
- min_order_size: 5 USDT
- allowed_profiles: LONG, SHORT only (no TWO_SIDED)

## Every tick

### 1 β€” Kalman signal
```
manage_routines(action="run", name="kalman_regime_check", config={
"connector_name": "bitget_perpetual",
"trading_pair": "BTC-USDT",
"candle_interval": "1m",
"lookback_candles": 60
})
```
Extract from result: regime, profile, snr, grid_D, grid_start, grid_end, limit_price.

### 2 β€” Executor state
```
manage_executors(action="list_executors")
```
Find any active grid_executor for BTC-USDT on bitget_perpetual. Note: executor_id, net_pnl_quote, filled_amount_quote, created_timestamp.

### 3 β€” Decide (first match wins)

**No executor running:**
- profile LONG or SHORT β†’ DEPLOY
- else β†’ HOLD

**Executor running:**
1. fills_amount_quote unchanged 10+ ticks β†’ RETUNE (teardown + redeploy same direction)
2. net_pnl_quote β‰₯ $2.00 β†’ PROFIT_TAKE (teardown + redeploy if signal still directional)
3. current grid_D differs >20% from deployed D β†’ RETUNE (teardown + redeploy updated range)
4. profile flipped AND executor age β‰₯ 180 min β†’ FLIP (teardown + deploy new direction)
5. else β†’ KEEP

### 4 β€” Act

**DEPLOY grid:**
```
manage_executors(action="create_executor", executor_config={
"type": "grid_executor",
"connector_name": "bitget_perpetual",
"trading_pair": "BTC-USDT",
"start_price": <grid_start from Kalman>,
"end_price": <grid_end from Kalman>,
"limit_price": <limit from Kalman>,
"total_amount_quote": 90,
"n_levels": 9,
"min_spread_between_orders": 0.001,
"min_order_amount_quote": 5,
"leverage": 5,
"side": "BUY" if LONG else "SELL",
"time_limit": 86400,
"keep_position": false,
"triple_barrier_config": {
"take_profit": 0.003,
"stop_loss": 0.02,
"stop_loss_order_type": "MARKET"
}
})
```

**STOP grid:**
```
manage_executors(action="stop_executor", executor_id=<id>)
```
Always verify flat (net position = 0) before redeploying.

### 5 β€” Journal every tick
```
trading_agent_journal_write(entries=[
{"key": "regime", "value": <regime>},
{"key": "snr", "value": <snr>},
{"key": "grid_D", "value": <grid_D>},
{"key": "profile", "value": <profile>},
{"key": "action", "value": <action taken>},
{"key": "pnl", "value": <net_pnl_quote or 0>},
{"key": "fills", "value": <filled_amount_quote or 0>}
])
```

## Safety β€” abort and alert if:
- Available balance < $90 before deploy
- Worst-case loss at limit_price > $10
- Liquidation price inside limit_price at 5x
- Position cannot be verified flat after teardown

Loading
Loading