almanak-strategy-builder by almanak-co/almanak-sdk
npx skills add https://github.com/almanak-co/almanak-sdk --skill almanak-strategy-builder您正在帮助一位量化分析师使用 Almanak SDK 构建 DeFi 策略。策略是返回 Intent 对象的 Python 类。框架负责编译成交易、执行和状态管理。
# 安装
pip install almanak
# 搭建新策略
almanak strat new --template mean_reversion --name my_rsi --chain arbitrum
# 在本地 Anvil 分叉上运行(自动启动网关 + Anvil)
cd my_rsi
almanak strat run --network anvil --once
# 在主网上运行单次迭代
almanak strat run --once
# 浏览并复制一个可用的演示策略
almanak strat demo
最小策略(3个文件):
my_strategy/
strategy.py # 包含 decide() 方法的 IntentStrategy 子类
config.json # 运行时参数(代币、阈值、资金)
.env # 密钥(ALMANAK_PRIVATE_KEY, ALCHEMY_API_KEY)
对于 Anvil 测试,在 config.json 中添加 anvil_funding,以便在分叉启动时自动为您的钱包注资(见下文配置)。
# strategy.py
from decimal import Decimal
from almanak.framework.strategies import IntentStrategy, MarketSnapshot, almanak_strategy
from almanak.framework.intents import Intent
@almanak_strategy(
name="my_strategy",
version="1.0.0",
supported_chains=["arbitrum"],
supported_protocols=["uniswap_v3"],
intent_types=["SWAP", "HOLD"],
default_chain="arbitrum",
)
class MyStrategy(IntentStrategy):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.trade_size = Decimal(str(self.config.get("trade_size_usd", "100")))
def decide(self, market: MarketSnapshot) -> Intent | None:
rsi = market.rsi("WETH", period=14)
if rsi.value < 30:
return Intent.swap(
from_token="USDC", to_token="WETH",
amount_usd=self.trade_size, max_slippage=Decimal("0.005"),
)
return Intent.hold(reason=f"RSI={rsi.value:.1f}, waiting")
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
注意:
amount_usd=需要网关提供实时价格预言机。如果交换因“收到太少”而回退,请切换到amount=(代币单位),这会绕过美元到代币的转换。首次实时运行时,始终使用--dry-run --once验证定价。
所有策略都继承自 IntentStrategy 并实现一个方法:
def decide(self, market: MarketSnapshot) -> Intent | None
框架在每次迭代时使用新的 MarketSnapshot 调用 decide()。返回一个 Intent 对象(交换、LP、借贷等)或 Intent.hold()。
__init__:提取配置参数,设置状态decide(market):每次迭代调用 - 返回一个 Intenton_intent_executed(intent, success, result):执行后的可选回调get_status():可选 - 返回用于监控仪表板的字典supports_teardown() / generate_teardown_intents():可选的安全关闭附加框架和 CLI 使用的元数据:
@almanak_strategy(
name="my_strategy", # 唯一标识符
description="What it does", # 人类可读的描述
version="1.0.0", # 策略版本
author="Your Name", # 可选
tags=["trading", "rsi"], # 用于发现的可选标签
supported_chains=["arbitrum"], # 此策略运行的链
supported_protocols=["uniswap_v3"], # 它使用的协议
intent_types=["SWAP", "HOLD"], # 它可能返回的 Intent 类型
default_chain="arbitrum", # 默认执行链
)
在 __init__ 中,从 self.config(从 config.json 加载的字典)读取参数:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.trade_size = Decimal(str(self.config.get("trade_size_usd", "100")))
self.rsi_period = int(self.config.get("rsi_period", 14))
self.base_token = self.config.get("base_token", "WETH")
也可用:self.chain (str), self.wallet_address (str)。
所有 Intent 都是通过 Intent 工厂方法创建的。导入:
from almanak.framework.intents import Intent
Intent.swap - 在 DEX 上交换代币
Intent.swap(
from_token="USDC", # 要卖出的代币
to_token="WETH", # 要买入的代币
amount_usd=Decimal("1000"), # 以 USD 计价的金额(使用 amount_usd 或 amount)
amount=Decimal("500"), # 以代币单位计价的金额(替代 amount_usd)
max_slippage=Decimal("0.005"), # 最大滑点(0.5%)
protocol="uniswap_v3", # 可选:特定 DEX
chain="arbitrum", # 可选:覆盖链
destination_chain="base", # 可选:跨链交换
)
使用 amount="all" 交换全部余额。
amount= 与 amount_usd=:使用 amount_usd= 指定以 USD 计价的交易规模(需要网关提供实时价格预言机)。使用 amount= 指定确切的代币单位(对于实时交易更可靠,因为它绕过了 USD 到代币的转换)。如有疑问,主网交易优先使用 amount=。
Intent.lp_open - 开设一个集中流动性 LP 头寸
Intent.lp_open(
pool="WETH/USDC", # 池标识符
amount0=Decimal("1.0"), # token0 的数量
amount1=Decimal("2000"), # token1 的数量
range_lower=Decimal("1800"), # 价格下限
range_upper=Decimal("2200"), # 价格上限
protocol="uniswap_v3", # 默认:uniswap_v3
chain=None, # 可选覆盖
)
Intent.lp_close - 关闭一个 LP 头寸
Intent.lp_close(
position_id="12345", # 来自 lp_open 结果的 NFT 代币 ID
pool="WETH/USDC", # 可选的池标识符
collect_fees=True, # 收取累积的费用
protocol="uniswap_v3",
)
Intent.collect_fees - 在不关闭的情况下收取 LP 费用
Intent.collect_fees(
pool="WETH/USDC",
protocol="traderjoe_v2",
)
Intent.supply - 将抵押品存入借贷协议
Intent.supply(
protocol="aave_v3",
token="WETH",
amount=Decimal("10"),
use_as_collateral=True, # 启用作为抵押品(默认:True)
market_id=None, # Morpho Blue 必需
)
Intent.borrow - 抵押借入代币
Intent.borrow(
protocol="aave_v3",
collateral_token="WETH",
collateral_amount=Decimal("10"),
borrow_token="USDC",
borrow_amount=Decimal("5000"),
interest_rate_mode="variable", # Aave:"variable" 或 "stable"
market_id=None, # Morpho Blue 必需
)
Intent.repay - 偿还借入的代币
Intent.repay(
protocol="aave_v3",
token="USDC",
amount=Decimal("5000"),
repay_full=False, # 设置为 True 以偿还全部债务
market_id=None,
)
Intent.withdraw - 从借贷协议中提取
Intent.withdraw(
protocol="aave_v3",
token="WETH",
amount=Decimal("10"),
withdraw_all=False, # 设置为 True 以提取全部
market_id=None,
)
Intent.perp_open - 开设永续期货头寸
Intent.perp_open(
market="ETH/USD",
collateral_token="USDC",
collateral_amount=Decimal("1000"),
size_usd=Decimal("5000"),
is_long=True,
leverage=Decimal("5"),
max_slippage=Decimal("0.01"),
protocol="gmx_v2",
)
Intent.perp_close - 关闭永续期货头寸
Intent.perp_close(
market="ETH/USD",
collateral_token="USDC",
is_long=True,
size_usd=None, # None = 关闭全部头寸
max_slippage=Decimal("0.01"),
protocol="gmx_v2",
)
Intent.bridge - 跨链代币转移
Intent.bridge(
token="USDC",
amount=Decimal("1000"),
from_chain="arbitrum",
to_chain="base",
max_slippage=Decimal("0.005"),
preferred_bridge=None, # 可选:特定桥接协议
)
Intent.stake - 流动性质押存款
Intent.stake(
protocol="lido",
token_in="ETH",
amount=Decimal("10"),
receive_wrapped=True, # 接收包装代币(例如,wstETH)
)
Intent.unstake - 从流动性质押中提取
Intent.unstake(
protocol="lido",
token_in="wstETH",
amount=Decimal("10"),
)
Intent.flash_loan - 在单笔交易中借入并偿还
Intent.flash_loan(
provider="aave", # "aave", "balancer", "morpho", 或 "auto"
token="USDC",
amount=Decimal("100000"),
callback_intents=[...], # 使用借入资金执行的 Intent
)
Intent.vault_deposit - 存入 ERC-4626 金库
Intent.vault_deposit(
vault="0x...", # 金库合约地址
asset_token="USDC",
amount=Decimal("1000"),
)
Intent.vault_redeem - 从 ERC-4626 金库赎回份额
Intent.vault_redeem(
vault="0x...",
shares_amount=Decimal("1000"),
)
Intent.prediction_buy(protocol="polymarket", market="...", amount_usd=Decimal("100"))
Intent.prediction_sell(protocol="polymarket", market="...", amount_shares=Decimal("50"))
Intent.prediction_redeem(protocol="polymarket", market="...")
Intent.ensure_balance - 元 Intent,解析为 BridgeIntent(如果余额不足)或 HoldIntent(如果已满足)。在从 decide() 返回之前调用 .resolve(market)。
intent = Intent.ensure_balance(
token="USDC",
min_amount=Decimal("1000"),
target_chain="arbitrum",
max_slippage=Decimal("0.005"),
preferred_bridge=None,
)
# 必须在返回前解析 - 返回 BridgeIntent 或 HoldIntent
resolved = intent.resolve(market)
return resolved
UnwrapNativeIntent - 解包原生代币(WETH -> ETH, WMATIC -> MATIC 等)
from almanak.framework.intents import UnwrapNativeIntent
from decimal import Decimal
UnwrapNativeIntent(
token="WETH", # 包装代币符号
amount=Decimal("0.5"), # 要解包的数量(或 "all")
chain="arbitrum", # 目标链
)
Intent.hold - 本次迭代不执行任何操作
Intent.hold(reason="RSI in neutral zone")
Intent.sequence - 按顺序执行多个 Intent
Intent.sequence(
intents=[
Intent.swap(from_token="USDC", to_token="WETH", amount_usd=Decimal("1000")),
Intent.supply(protocol="aave_v3", token="WETH", amount=Decimal("0.5")),
],
description="Buy WETH then supply to Aave",
)
使用 "all" 来引用前一个 Intent 的全部输出:
Intent.sequence(intents=[
Intent.swap(from_token="USDC", to_token="WETH", amount_usd=Decimal("1000")),
Intent.supply(protocol="aave_v3", token="WETH", amount="all"), # 使用交换输出
])
传递给 decide() 的 MarketSnapshot 提供以下方法:
price = market.price("WETH") # Decimal, USD 价格
price = market.price("WETH", quote="USDC") # 以 USDC 计价的价格
pd = market.price_data("WETH") # PriceData 对象
pd.price # Decimal - 当前价格
pd.price_24h_ago # Decimal
pd.change_24h_pct # Decimal
pd.high_24h # Decimal
pd.low_24h # Decimal
pd.timestamp # datetime
bal = market.balance("USDC")
bal.balance # Decimal - 代币数量
bal.balance_usd # Decimal - USD 价值
bal.symbol # str
bal.address # str - 代币合约地址
TokenBalance 支持数值比较:bal > Decimal("100")。
所有指标都接受 token、period (int) 和 timeframe (str, 默认 "4h")。
rsi = market.rsi("WETH", period=14, timeframe="4h")
rsi.value # Decimal (0-100)
rsi.is_oversold # bool (value < 30)
rsi.is_overbought # bool (value > 70)
rsi.signal # "BUY" | "SELL" | "HOLD"
macd = market.macd("WETH", fast_period=12, slow_period=26, signal_period=9)
macd.macd_line # Decimal
macd.signal_line # Decimal
macd.histogram # Decimal
macd.is_bullish_crossover # bool
macd.is_bearish_crossover # bool
bb = market.bollinger_bands("WETH", period=20, std_dev=2.0)
bb.upper_band # Decimal
bb.middle_band # Decimal
bb.lower_band # Decimal
bb.bandwidth # Decimal
bb.percent_b # Decimal (0.0 = 在下轨,1.0 = 在上轨)
bb.is_squeeze # bool
stoch = market.stochastic("WETH", k_period=14, d_period=3)
stoch.k_value # Decimal
stoch.d_value # Decimal
stoch.is_oversold # bool
stoch.is_overbought # bool
atr_val = market.atr("WETH", period=14)
atr_val.value # Decimal (绝对值)
atr_val.value_percent # Decimal,百分比点(2.62 表示 2.62%,不是 0.0262)
atr_val.is_high_volatility # bool
sma = market.sma("WETH", period=20)
ema = market.ema("WETH", period=12)
# 两者都返回 MAData,包含:.value, .is_price_above, .is_price_below, .signal
adx = market.adx("WETH", period=14)
adx.adx # Decimal (0-100, 趋势强度)
adx.plus_di # Decimal (+DI)
adx.minus_di # Decimal (-DI)
adx.is_strong_trend # bool (adx >= 25)
adx.is_uptrend # bool (+DI > -DI)
adx.is_downtrend # bool (-DI > +DI)
obv = market.obv("WETH")
obv.obv # Decimal (OBV 值)
obv.signal_line # Decimal (OBV 的 SMA)
obv.is_bullish # bool (OBV > signal)
obv.is_bearish # bool (OBV < signal)
cci = market.cci("WETH", period=20)
cci.value # Decimal
cci.is_oversold # bool (value <= -100)
cci.is_overbought # bool (value >= 100)
ich = market.ichimoku("WETH")
ich.tenkan_sen # Decimal (转换线)
ich.kijun_sen # Decimal (基准线)
ich.senkou_span_a # Decimal (先行跨度 A)
ich.senkou_span_b # Decimal (先行跨度 B)
ich.cloud_top # Decimal
ich.cloud_bottom # Decimal
ich.is_bullish_crossover # bool (tenkan > kijun)
ich.is_above_cloud # bool
ich.signal # "BUY" | "SELL" | "HOLD"
prices = market.prices(["WETH", "WBTC"]) # dict[str, Decimal]
balances = market.balances(["USDC", "WETH"]) # dict[str, Decimal]
usd_val = market.balance_usd("WETH") # Decimal - 持仓的 USD 价值
total = market.total_portfolio_usd() # Decimal
# 任意抵押品数量的 USD 价值(用于永续头寸规模)
col_usd = market.collateral_value_usd("WETH", Decimal("2")) # Decimal - 数量 * 价格
df = market.ohlcv("WETH", timeframe="1h", limit=100) # pd.DataFrame
# 列:open, high, low, close, volume
pool = market.pool_price("0x...") # DataEnvelope[PoolPrice]
pool = market.pool_price_by_pair("WETH", "USDC") # DataEnvelope[PoolPrice]
reserves = market.pool_reserves("0x...") # PoolReserves
history = market.pool_history("0x...", resolution="1h") # DataEnvelope[list[PoolSnapshot]]
analytics = market.pool_analytics("0x...") # DataEnvelope[PoolAnalytics]
best = market.best_pool("WETH", "USDC", metric="fee_apr") # DataEnvelope[PoolAnalyticsResult]
twap = market.twap("WETH/USDC", window_seconds=300) # DataEnvelope[AggregatedPrice]
lwap = market.lwap("WETH/USDC") # DataEnvelope[AggregatedPrice]
depth = market.liquidity_depth("0x...") # DataEnvelope[LiquidityDepth]
slip = market.estimate_slippage("WETH", "USDC", Decimal("10000")) # DataEnvelope[SlippageEstimate]
best_dex = market.best_dex_price("WETH", "USDC", Decimal("1")) # BestDexResult
rate = market.lending_rate("aave_v3", "USDC", side="supply") # LendingRate
best = market.best_lending_rate("USDC", side="supply") # BestRateResult
fr = market.funding_rate("binance", "ETH-PERP") # FundingRate
spread = market.funding_rate_spread("ETH-PERP", "binance", "hyperliquid") # FundingRateSpread
il = market.il_exposure("position_id", fees_earned=Decimal("50")) # ILExposure
proj = market.projected_il("WETH", "USDC", price_change_pct=Decimal("0.1")) # ProjectedILResult
mkt = market.prediction("market_id") # PredictionMarket
price = market.prediction_price("market_id", "YES") # Decimal
positions = market.prediction_positions("market_id") # list[PredictionPosition]
orders = market.prediction_orders("market_id") # list[PredictionOrder]
yields = market.yield_opportunities("USDC", min_tvl=100_000, sort_by="apy") # DataEnvelope[list[YieldOpportunity]]
gas = market.gas_price() # GasPrice
health = market.health() # HealthReport
signals = market.wallet_activity(action_types=["SWAP", "LP_OPEN"]) # list
market.chain # str - 当前链名称
market.wallet_address # str - 钱包地址
market.timestamp # datetime - 快照时间戳
self.state 是一个在迭代和重启之间持久化的字典。运行器在启动时调用 load_state(),从 StateManager(本地为 SQLite,部署时为网关)恢复之前保存的状态。在 decide() 或 on_intent_executed() 期间写入 self.state,更改会在每次迭代后自动持久化。
def decide(self, market):
# 读取状态(在重启后保留)
last_trade = self.state.get("last_trade_time")
position_id = self.state.get("lp_position_id")
# 写入状态(每次迭代后自动持久化)
self.state["last_trade_time"] = datetime.now().isoformat()
self.state["consecutive_holds"] = self.state.get("consecutive_holds", 0) + 1
使用 --fresh 在重新开始时清除保存的状态:almanak strat run --fresh --once。
对于需要自定义序列化(例如,Decimal 字段、复杂对象)的策略,覆盖 get_persistent_state() 和 load_persistent_state():
def get_persistent_state(self) -> dict:
"""由框架调用以序列化状态进行持久化。"""
return {
"phase": self.state.get("phase", "idle"),
"position_id": self.state.get("position_id"),
"entry_price": str(self.state.get("entry_price", "0")),
}
def load_persistent_state(self, saved: dict) -> None:
"""由框架在启动时调用以恢复状态。"""
self.state["phase"] = saved.get("phase", "idle")
self.state["position_id"] = saved.get("position_id")
self.state["entry_price"] = Decimal(saved.get("entry_price", "0"))
执行后,通过回调访问结果(头寸 ID、交换数量)。框架自动用协议特定的数据丰富 result - 无需手动解析收据。
# 在您的策略文件顶部,导入 logging:
# import logging
# logger = logging.getLogger(__name__)
def on_intent_executed(self, intent, success: bool, result):
if not success:
logger.warning(f"Intent failed: {intent.intent_type}")
return
# 捕获 LP 头寸 ID(由 ResultEnricher 自动丰富)
if result.position_id is not None:
self.state["lp_position_id"] = result.position_id
logger.info(f"Opened LP position {result.position_id}")
# 为再平衡策略存储范围边界(保持为 Decimal)
if (
hasattr(intent, "range_lower") and intent.range_lower is not None
and hasattr(intent, "range_upper") and intent.range_upper is not None
):
self.state["range_lower"] = intent.range_lower
self.state["range_upper"] = intent.range_upper
# 捕获交换数量
if result.swap_amounts:
self.state["last_swap"] = {
"amount_in": str(result.swap_amounts.amount_in),
"amount_out": str(result.swap_amounts.amount_out),
}
logger.info(
f"Swapped {result.swap_amounts.amount_in} -> {result.swap_amounts.amount_out}"
)
仅包含可调整的运行时参数。结构性元数据(名称、描述、默认执行链)位于策略类的 @almanak_strategy 装饰器中。
{
"base_token": "WETH",
"quote_token": "USDC",
"rsi_period": 14,
"rsi_oversold": 30,
"rsi_overbought": 70,
"trade_size_usd": 1000,
"max_slippage_bps": 50,
"anvil_funding": {
"USDC": "10000",
"WETH": "5"
}
}
没有必填字段 - 所有字段都是策略特定的,通过 self.config.get(key, default) 访问。默认执行链来自 @almanak_strategy 装饰器中的 default_chain(如果省略,则回退到 supported_chains[0])。
# 必需
ALMANAK_PRIVATE_KEY=0x...
# RPC 访问(至少设置一个)
ALCHEMY_API_KEY=your_key
# RPC_URL=https://...
# 可选
# ENSO_API_KEY=
# COINGECKO_API_KEY=
# ALMANAK_API_KEY=
在 Anvil 上运行时(--network anvil),框架会自动用 anvil_funding 中指定的代币为钱包注资。值以代币单位表示(不是 USD)。
对所有代币查找使用 get_token_resolver()。切勿硬编码地址。
from almanak.framework.data.tokens import get_token_resolver
resolver = get_token_resolver()
# 通过符号解析
token = resolver.resolve("USDC", "arbitrum")
# -> ResolvedToken(symbol="USDC", address="0xaf88...", decimals=6, chain="arbitrum")
# 通过地址解析
token = resolver.resolve("0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "arbitrum")
# 便捷方法
decimals = resolver.get_decimals("arbitrum", "USDC") # -> 6
address = resolver.get_address("arbitrum", "USDC") # -> "0xaf88..."
# 对于 DEX 交换(自动包装原生代币:ETH->WETH, MATIC->WMATIC)
token = resolver.resolve_for_swap("ETH", "arbitrum") # -> WETH
# 解析交易对
usdc, weth = resolver.resolve_pair("USDC", "WETH", "arbitrum")
解析顺序:内存缓存 -> 磁盘缓存 -> 静态注册表 -> 网关链上查找。
切勿默认使用 18 位小数。如果代币未知,会引发 TokenNotFoundError。
almanak strat backtest pnl -s my_strategy \
--start 2024-01-01 --end 2024-06-01 \
--initial-capital 10000
almanak strat backtest paper -s my_strategy \
--duration 3600 --interval 60 \
--initial-capital 10000
模拟交易在 Anvil 分叉上运行完整的策略循环,包含真实的交易执行、权益曲线跟踪和 JSON 结果日志。
almanak strat backtest sweep -s my_strategy \
--start 2024-01-01 --end 2024-06-01 \
--param "rsi_oversold:20,25,30" \
--param "rsi_overbought:70,75,80"
在所有参数组合上运行 PnL 回测,并按夏普比率排序。
from almanak.framework.backtesting import BacktestEngine
engine = BacktestEngine(
strategy_class=MyStrategy,
config={...},
start_date="2024-01-01",
end_date="2024-06-01",
initial_capital=10000,
)
results = engine.run()
results.sharpe_ratio
results.max_drawdown
results.total_return
results.plot() # Matplotlib 权益曲线
backtest pnl、backtest paper、backtest sweep)需要显式的 -s strategy_name 标志。它们不会像 strat run 那样从当前目录自动发现策略。total_return_pct 和类似的 _pct 结果字段是十进制分数(0.33 = 33%),不是百分比。almanak strat new # 交互式策略搭建
almanak strat new -t mean_reversion -n my_rsi -c arbitrum # 非交互式
almanak strat demo # 浏览并复制一个可用的演示策略
模板: blank, dynamic_lp, mean_reversion, bollinger, basis_trade, lending_loop, copy_trader
almanak strat run --once # 单次迭代(从策略目录)
almanak strat run -d path/to/strat --once # 显式目录
almanak strat run --network anvil --once # 本地 Anvil 分叉
almanak strat run --interval 30 # 连续运行(迭代间隔 30 秒)
almanak strat run --dry-run --once # 不提交交易
almanak strat run --fresh --once # 运行前清除状态
almanak strat run --id abc123 --once # 恢复之前的运行
almanak strat run --dashboard # 启动实时监控仪表板
almanak strat backtest pnl -s my_strategy # 历史 PnL 模拟
almanak strat backtest paper -s my_strategy # Anvil 分叉上的模拟交易
almanak strat backtest sweep -s my_strategy # 参数扫描优化
almanak strat teardown plan # 预览关闭清理 Intent
almanak strat teardown execute # 执行关闭清理
almanak gateway # 启动独立网关
almanak gateway --network anvil # 用于本地 Anvil 测试的网关
almanak gateway --port 50052 # 自定义端口
almanak agent install # 自动检测平台并安装
almanak agent install -p claude # 为特定平台安装
almanak agent install -p all # 为所有 9 个平台安装
almanak agent update # 更新已安装的技能文件
almanak agent status # 检查安装状态
almanak docs path # 捆绑的 LLM 文档路径
almanak docs dump # 打印完整的 LLM 文档
almanak docs agent-skill # 捆绑的代理技能路径
almanak docs agent-skill --dump # 打印代理技能内容
| 链 | 枚举值 | 配置名称 |
|---|---|---|
| Ethereum | ETHEREUM | ethereum |
| Arbitrum | ARBITRUM | arbitrum |
| Optimism | OPTIMISM | optimism |
| Base | BASE | base |
| Avalanche | AVALANCHE | avalanche |
| Polygon | POLYGON | polygon |
| BSC | BSC | bsc |
| Sonic | SONIC | sonic |
| Plasma | PLASMA | plasma |
| Blast | BLAST | blast |
| Mantle | MANTLE | mantle |
| Berachain | BERACHAIN | berachain |
| Monad | MONAD | monad |
| 协议 | 枚举值 | 类型 | 配置名称 |
|---|---|---|---|
| Uniswap V3 | UNISWAP_V3 | DEX / LP | uniswap_v3 |
| PancakeSwap V3 | PANCAKESWAP_V3 | DEX / LP | pancakeswap_v3 |
| SushiSwap V3 | SUSHISWAP_V3 | DEX / LP | sushiswap_v3 |
| TraderJoe V2 | TRADERJOE_V2 | DEX / LP | traderjoe_v2 |
| Aerodrome | AERODROME | DEX / LP | aerodrome |
| Enso | ENSO | 聚合器 | enso |
| Pendle | PENDLE | 收益 | pendle |
| MetaMorpho | METAMORPHO | 借贷 | metamorpho |
| LiFi | LIFI | 桥接 | lifi |
| Vault | VAULT |
You are helping a quant build DeFi strategies using the Almanak SDK. Strategies are Python classes that return Intent objects. The framework handles compilation to transactions, execution, and state management.
# Install
pip install almanak
# Scaffold a new strategy
almanak strat new --template mean_reversion --name my_rsi --chain arbitrum
# Run on local Anvil fork (auto-starts gateway + Anvil)
cd my_rsi
almanak strat run --network anvil --once
# Run a single iteration on mainnet
almanak strat run --once
# Browse and copy a working demo strategy
almanak strat demo
Minimal strategy (3 files):
my_strategy/
strategy.py # IntentStrategy subclass with decide() method
config.json # Runtime parameters (tokens, thresholds, funding)
.env # Secrets (ALMANAK_PRIVATE_KEY, ALCHEMY_API_KEY)
For Anvil testing, add anvil_funding to config.json so your wallet is auto-funded on fork start (see Configuration below).
# strategy.py
from decimal import Decimal
from almanak.framework.strategies import IntentStrategy, MarketSnapshot, almanak_strategy
from almanak.framework.intents import Intent
@almanak_strategy(
name="my_strategy",
version="1.0.0",
supported_chains=["arbitrum"],
supported_protocols=["uniswap_v3"],
intent_types=["SWAP", "HOLD"],
default_chain="arbitrum",
)
class MyStrategy(IntentStrategy):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.trade_size = Decimal(str(self.config.get("trade_size_usd", "100")))
def decide(self, market: MarketSnapshot) -> Intent | None:
rsi = market.rsi("WETH", period=14)
if rsi.value < 30:
return Intent.swap(
from_token="USDC", to_token="WETH",
amount_usd=self.trade_size, max_slippage=Decimal("0.005"),
)
return Intent.hold(reason=f"RSI={rsi.value:.1f}, waiting")
Note:
amount_usd=requires a live price oracle from the gateway. If swaps revert with "Too little received", switch toamount=(token units) which bypasses USD-to-token conversion. Always verify pricing on first live run with--dry-run --once.
All strategies inherit from IntentStrategy and implement one method:
def decide(self, market: MarketSnapshot) -> Intent | None
The framework calls decide() on each iteration with a fresh MarketSnapshot. Return an Intent object (swap, LP, borrow, etc.) or Intent.hold().
__init__: Extract config parameters, set up statedecide(market): Called each iteration - return an Intenton_intent_executed(intent, success, result): Optional callback after executionget_status(): Optional - return dict for monitoring dashboardssupports_teardown() / generate_teardown_intents(): Optional safe shutdownAttaches metadata used by the framework and CLI:
@almanak_strategy(
name="my_strategy", # Unique identifier
description="What it does", # Human-readable description
version="1.0.0", # Strategy version
author="Your Name", # Optional
tags=["trading", "rsi"], # Optional tags for discovery
supported_chains=["arbitrum"], # Which chains this runs on
supported_protocols=["uniswap_v3"], # Which protocols it uses
intent_types=["SWAP", "HOLD"], # Intent types it may return
default_chain="arbitrum", # Default chain for execution
)
In __init__, read parameters from self.config (dict loaded from config.json):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.trade_size = Decimal(str(self.config.get("trade_size_usd", "100")))
self.rsi_period = int(self.config.get("rsi_period", 14))
self.base_token = self.config.get("base_token", "WETH")
Also available: self.chain (str), self.wallet_address (str).
All intents are created via Intent factory methods. Import:
from almanak.framework.intents import Intent
Intent.swap - Exchange tokens on a DEX
Intent.swap(
from_token="USDC", # Token to sell
to_token="WETH", # Token to buy
amount_usd=Decimal("1000"), # Amount in USD (use amount_usd OR amount)
amount=Decimal("500"), # Amount in token units (alternative to amount_usd)
max_slippage=Decimal("0.005"), # Max slippage (0.5%)
protocol="uniswap_v3", # Optional: specific DEX
chain="arbitrum", # Optional: override chain
destination_chain="base", # Optional: cross-chain swap
)
Use amount="all" to swap the entire balance.
amount= vs amount_usd=: Use amount_usd= to specify trade size in USD (requires a live price oracle from the gateway). Use amount= to specify exact token units (more reliable for live trading since it bypasses USD-to-token conversion). When in doubt, prefer amount= for mainnet.
Intent.lp_open - Open a concentrated LP position
Intent.lp_open(
pool="WETH/USDC", # Pool identifier
amount0=Decimal("1.0"), # Amount of token0
amount1=Decimal("2000"), # Amount of token1
range_lower=Decimal("1800"), # Lower price bound
range_upper=Decimal("2200"), # Upper price bound
protocol="uniswap_v3", # Default: uniswap_v3
chain=None, # Optional override
)
Intent.lp_close - Close an LP position
Intent.lp_close(
position_id="12345", # NFT token ID from lp_open result
pool="WETH/USDC", # Optional pool identifier
collect_fees=True, # Collect accumulated fees
protocol="uniswap_v3",
)
Intent.collect_fees - Harvest LP fees without closing
Intent.collect_fees(
pool="WETH/USDC",
protocol="traderjoe_v2",
)
Intent.supply - Deposit collateral into a lending protocol
Intent.supply(
protocol="aave_v3",
token="WETH",
amount=Decimal("10"),
use_as_collateral=True, # Enable as collateral (default: True)
market_id=None, # Required for Morpho Blue
)
Intent.borrow - Borrow tokens against collateral
Intent.borrow(
protocol="aave_v3",
collateral_token="WETH",
collateral_amount=Decimal("10"),
borrow_token="USDC",
borrow_amount=Decimal("5000"),
interest_rate_mode="variable", # Aave: "variable" or "stable"
market_id=None, # Required for Morpho Blue
)
Intent.repay - Repay borrowed tokens
Intent.repay(
protocol="aave_v3",
token="USDC",
amount=Decimal("5000"),
repay_full=False, # Set True to repay entire debt
market_id=None,
)
Intent.withdraw - Withdraw from lending protocol
Intent.withdraw(
protocol="aave_v3",
token="WETH",
amount=Decimal("10"),
withdraw_all=False, # Set True to withdraw everything
market_id=None,
)
Intent.perp_open - Open a perpetual futures position
Intent.perp_open(
market="ETH/USD",
collateral_token="USDC",
collateral_amount=Decimal("1000"),
size_usd=Decimal("5000"),
is_long=True,
leverage=Decimal("5"),
max_slippage=Decimal("0.01"),
protocol="gmx_v2",
)
Intent.perp_close - Close a perpetual futures position
Intent.perp_close(
market="ETH/USD",
collateral_token="USDC",
is_long=True,
size_usd=None, # None = close full position
max_slippage=Decimal("0.01"),
protocol="gmx_v2",
)
Intent.bridge - Cross-chain token transfer
Intent.bridge(
token="USDC",
amount=Decimal("1000"),
from_chain="arbitrum",
to_chain="base",
max_slippage=Decimal("0.005"),
preferred_bridge=None, # Optional: specific bridge protocol
)
Intent.stake - Liquid staking deposit
Intent.stake(
protocol="lido",
token_in="ETH",
amount=Decimal("10"),
receive_wrapped=True, # Receive wrapped token (e.g., wstETH)
)
Intent.unstake - Withdraw from liquid staking
Intent.unstake(
protocol="lido",
token_in="wstETH",
amount=Decimal("10"),
)
Intent.flash_loan - Borrow and repay in a single transaction
Intent.flash_loan(
provider="aave", # "aave", "balancer", "morpho", or "auto"
token="USDC",
amount=Decimal("100000"),
callback_intents=[...], # Intents to execute with the borrowed funds
)
Intent.vault_deposit - Deposit into an ERC-4626 vault
Intent.vault_deposit(
vault="0x...", # Vault contract address
asset_token="USDC",
amount=Decimal("1000"),
)
Intent.vault_redeem - Redeem shares from an ERC-4626 vault
Intent.vault_redeem(
vault="0x...",
shares_amount=Decimal("1000"),
)
Intent.prediction_buy(protocol="polymarket", market="...", amount_usd=Decimal("100"))
Intent.prediction_sell(protocol="polymarket", market="...", amount_shares=Decimal("50"))
Intent.prediction_redeem(protocol="polymarket", market="...")
Intent.ensure_balance - Meta-intent that resolves to a BridgeIntent (if balance is insufficient) or HoldIntent (if already met). Call .resolve(market) before returning from decide().
intent = Intent.ensure_balance(
token="USDC",
min_amount=Decimal("1000"),
target_chain="arbitrum",
max_slippage=Decimal("0.005"),
preferred_bridge=None,
)
# Must resolve before returning - returns BridgeIntent or HoldIntent
resolved = intent.resolve(market)
return resolved
UnwrapNativeIntent - Unwrap wrapped native tokens (WETH -> ETH, WMATIC -> MATIC, etc.)
from almanak.framework.intents import UnwrapNativeIntent
from decimal import Decimal
UnwrapNativeIntent(
token="WETH", # Wrapped token symbol
amount=Decimal("0.5"), # Amount to unwrap (or "all")
chain="arbitrum", # Target chain
)
Intent.hold - Do nothing this iteration
Intent.hold(reason="RSI in neutral zone")
Intent.sequence - Execute multiple intents in order
Intent.sequence(
intents=[
Intent.swap(from_token="USDC", to_token="WETH", amount_usd=Decimal("1000")),
Intent.supply(protocol="aave_v3", token="WETH", amount=Decimal("0.5")),
],
description="Buy WETH then supply to Aave",
)
Use "all" to reference the full output of a prior intent:
Intent.sequence(intents=[
Intent.swap(from_token="USDC", to_token="WETH", amount_usd=Decimal("1000")),
Intent.supply(protocol="aave_v3", token="WETH", amount="all"), # Uses swap output
])
The MarketSnapshot passed to decide() provides these methods:
price = market.price("WETH") # Decimal, USD price
price = market.price("WETH", quote="USDC") # Price in USDC terms
pd = market.price_data("WETH") # PriceData object
pd.price # Decimal - current price
pd.price_24h_ago # Decimal
pd.change_24h_pct # Decimal
pd.high_24h # Decimal
pd.low_24h # Decimal
pd.timestamp # datetime
bal = market.balance("USDC")
bal.balance # Decimal - token amount
bal.balance_usd # Decimal - USD value
bal.symbol # str
bal.address # str - token contract address
TokenBalance supports numeric comparisons: bal > Decimal("100").
All indicators accept token, period (int), and timeframe (str, default "4h").
rsi = market.rsi("WETH", period=14, timeframe="4h")
rsi.value # Decimal (0-100)
rsi.is_oversold # bool (value < 30)
rsi.is_overbought # bool (value > 70)
rsi.signal # "BUY" | "SELL" | "HOLD"
macd = market.macd("WETH", fast_period=12, slow_period=26, signal_period=9)
macd.macd_line # Decimal
macd.signal_line # Decimal
macd.histogram # Decimal
macd.is_bullish_crossover # bool
macd.is_bearish_crossover # bool
bb = market.bollinger_bands("WETH", period=20, std_dev=2.0)
bb.upper_band # Decimal
bb.middle_band # Decimal
bb.lower_band # Decimal
bb.bandwidth # Decimal
bb.percent_b # Decimal (0.0 = at lower band, 1.0 = at upper band)
bb.is_squeeze # bool
stoch = market.stochastic("WETH", k_period=14, d_period=3)
stoch.k_value # Decimal
stoch.d_value # Decimal
stoch.is_oversold # bool
stoch.is_overbought # bool
atr_val = market.atr("WETH", period=14)
atr_val.value # Decimal (absolute)
atr_val.value_percent # Decimal, percentage points (2.62 means 2.62%, not 0.0262)
atr_val.is_high_volatility # bool
sma = market.sma("WETH", period=20)
ema = market.ema("WETH", period=12)
# Both return MAData with: .value, .is_price_above, .is_price_below, .signal
adx = market.adx("WETH", period=14)
adx.adx # Decimal (0-100, trend strength)
adx.plus_di # Decimal (+DI)
adx.minus_di # Decimal (-DI)
adx.is_strong_trend # bool (adx >= 25)
adx.is_uptrend # bool (+DI > -DI)
adx.is_downtrend # bool (-DI > +DI)
obv = market.obv("WETH")
obv.obv # Decimal (OBV value)
obv.signal_line # Decimal (SMA of OBV)
obv.is_bullish # bool (OBV > signal)
obv.is_bearish # bool (OBV < signal)
cci = market.cci("WETH", period=20)
cci.value # Decimal
cci.is_oversold # bool (value <= -100)
cci.is_overbought # bool (value >= 100)
ich = market.ichimoku("WETH")
ich.tenkan_sen # Decimal (conversion line)
ich.kijun_sen # Decimal (base line)
ich.senkou_span_a # Decimal (leading span A)
ich.senkou_span_b # Decimal (leading span B)
ich.cloud_top # Decimal
ich.cloud_bottom # Decimal
ich.is_bullish_crossover # bool (tenkan > kijun)
ich.is_above_cloud # bool
ich.signal # "BUY" | "SELL" | "HOLD"
prices = market.prices(["WETH", "WBTC"]) # dict[str, Decimal]
balances = market.balances(["USDC", "WETH"]) # dict[str, Decimal]
usd_val = market.balance_usd("WETH") # Decimal - USD value of holdings
total = market.total_portfolio_usd() # Decimal
# USD value of an arbitrary collateral amount (for perp position sizing)
col_usd = market.collateral_value_usd("WETH", Decimal("2")) # Decimal - amount * price
df = market.ohlcv("WETH", timeframe="1h", limit=100) # pd.DataFrame
# Columns: open, high, low, close, volume
pool = market.pool_price("0x...") # DataEnvelope[PoolPrice]
pool = market.pool_price_by_pair("WETH", "USDC") # DataEnvelope[PoolPrice]
reserves = market.pool_reserves("0x...") # PoolReserves
history = market.pool_history("0x...", resolution="1h") # DataEnvelope[list[PoolSnapshot]]
analytics = market.pool_analytics("0x...") # DataEnvelope[PoolAnalytics]
best = market.best_pool("WETH", "USDC", metric="fee_apr") # DataEnvelope[PoolAnalyticsResult]
twap = market.twap("WETH/USDC", window_seconds=300) # DataEnvelope[AggregatedPrice]
lwap = market.lwap("WETH/USDC") # DataEnvelope[AggregatedPrice]
depth = market.liquidity_depth("0x...") # DataEnvelope[LiquidityDepth]
slip = market.estimate_slippage("WETH", "USDC", Decimal("10000")) # DataEnvelope[SlippageEstimate]
best_dex = market.best_dex_price("WETH", "USDC", Decimal("1")) # BestDexResult
rate = market.lending_rate("aave_v3", "USDC", side="supply") # LendingRate
best = market.best_lending_rate("USDC", side="supply") # BestRateResult
fr = market.funding_rate("binance", "ETH-PERP") # FundingRate
spread = market.funding_rate_spread("ETH-PERP", "binance", "hyperliquid") # FundingRateSpread
il = market.il_exposure("position_id", fees_earned=Decimal("50")) # ILExposure
proj = market.projected_il("WETH", "USDC", price_change_pct=Decimal("0.1")) # ProjectedILResult
mkt = market.prediction("market_id") # PredictionMarket
price = market.prediction_price("market_id", "YES") # Decimal
positions = market.prediction_positions("market_id") # list[PredictionPosition]
orders = market.prediction_orders("market_id") # list[PredictionOrder]
yields = market.yield_opportunities("USDC", min_tvl=100_000, sort_by="apy") # DataEnvelope[list[YieldOpportunity]]
gas = market.gas_price() # GasPrice
health = market.health() # HealthReport
signals = market.wallet_activity(action_types=["SWAP", "LP_OPEN"]) # list
market.chain # str - current chain name
market.wallet_address # str - wallet address
market.timestamp # datetime - snapshot timestamp
self.state is a dict that persists across iterations and restarts. The runner calls load_state() on startup, restoring previously saved state from the StateManager (SQLite locally, gateway when deployed). Write to self.state during decide() or on_intent_executed() and changes are automatically persisted after each iteration.
def decide(self, market):
# Read state (survives restarts)
last_trade = self.state.get("last_trade_time")
position_id = self.state.get("lp_position_id")
# Write state (persisted automatically after each iteration)
self.state["last_trade_time"] = datetime.now().isoformat()
self.state["consecutive_holds"] = self.state.get("consecutive_holds", 0) + 1
Use --fresh to clear saved state when starting over: almanak strat run --fresh --once.
For strategies that need custom serialization (e.g., Decimal fields, complex objects), override get_persistent_state() and load_persistent_state():
def get_persistent_state(self) -> dict:
"""Called by framework to serialize state for persistence."""
return {
"phase": self.state.get("phase", "idle"),
"position_id": self.state.get("position_id"),
"entry_price": str(self.state.get("entry_price", "0")),
}
def load_persistent_state(self, saved: dict) -> None:
"""Called by framework on startup to restore state."""
self.state["phase"] = saved.get("phase", "idle")
self.state["position_id"] = saved.get("position_id")
self.state["entry_price"] = Decimal(saved.get("entry_price", "0"))
After execution, access results (position IDs, swap amounts) via the callback. The framework automatically enriches result with protocol-specific data - no manual receipt parsing needed.
# In your strategy file, import logging at the top:
# import logging
# logger = logging.getLogger(__name__)
def on_intent_executed(self, intent, success: bool, result):
if not success:
logger.warning(f"Intent failed: {intent.intent_type}")
return
# Capture LP position ID (enriched automatically by ResultEnricher)
if result.position_id is not None:
self.state["lp_position_id"] = result.position_id
logger.info(f"Opened LP position {result.position_id}")
# Store range bounds for rebalancing strategies (keep as Decimal)
if (
hasattr(intent, "range_lower") and intent.range_lower is not None
and hasattr(intent, "range_upper") and intent.range_upper is not None
):
self.state["range_lower"] = intent.range_lower
self.state["range_upper"] = intent.range_upper
# Capture swap amounts
if result.swap_amounts:
self.state["last_swap"] = {
"amount_in": str(result.swap_amounts.amount_in),
"amount_out": str(result.swap_amounts.amount_out),
}
logger.info(
f"Swapped {result.swap_amounts.amount_in} -> {result.swap_amounts.amount_out}"
)
Contains only tunable runtime parameters. Structural metadata (name, description, default execution chain) lives in the @almanak_strategy decorator on your strategy class.
{
"base_token": "WETH",
"quote_token": "USDC",
"rsi_period": 14,
"rsi_oversold": 30,
"rsi_overbought": 70,
"trade_size_usd": 1000,
"max_slippage_bps": 50,
"anvil_funding": {
"USDC": "10000",
"WETH": "5"
}
}
No required fields - all fields are strategy-specific and accessed via self.config.get(key, default). The default execution chain comes from default_chain in the @almanak_strategy decorator (falls back to supported_chains[0] if omitted).
# Required
ALMANAK_PRIVATE_KEY=0x...
# RPC access (set at least one)
ALCHEMY_API_KEY=your_key
# RPC_URL=https://...
# Optional
# ENSO_API_KEY=
# COINGECKO_API_KEY=
# ALMANAK_API_KEY=
When running on Anvil (--network anvil), the framework auto-funds the wallet with tokens specified in anvil_funding. Values are in token units (not USD).
Use get_token_resolver() for all token lookups. Never hardcode addresses.
from almanak.framework.data.tokens import get_token_resolver
resolver = get_token_resolver()
# Resolve by symbol
token = resolver.resolve("USDC", "arbitrum")
# -> ResolvedToken(symbol="USDC", address="0xaf88...", decimals=6, chain="arbitrum")
# Resolve by address
token = resolver.resolve("0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "arbitrum")
# Convenience
decimals = resolver.get_decimals("arbitrum", "USDC") # -> 6
address = resolver.get_address("arbitrum", "USDC") # -> "0xaf88..."
# For DEX swaps (auto-wraps native tokens: ETH->WETH, MATIC->WMATIC)
token = resolver.resolve_for_swap("ETH", "arbitrum") # -> WETH
# Resolve trading pair
usdc, weth = resolver.resolve_pair("USDC", "WETH", "arbitrum")
Resolution order: memory cache -> disk cache -> static registry -> gateway on-chain lookup.
Never default to 18 decimals. If the token is unknown, TokenNotFoundError is raised.
almanak strat backtest pnl -s my_strategy \
--start 2024-01-01 --end 2024-06-01 \
--initial-capital 10000
almanak strat backtest paper -s my_strategy \
--duration 3600 --interval 60 \
--initial-capital 10000
Paper trading runs the full strategy loop on an Anvil fork with real transaction execution, equity curve tracking, and JSON result logs.
almanak strat backtest sweep -s my_strategy \
--start 2024-01-01 --end 2024-06-01 \
--param "rsi_oversold:20,25,30" \
--param "rsi_overbought:70,75,80"
Runs the PnL backtest across all parameter combinations and ranks by Sharpe ratio.
from almanak.framework.backtesting import BacktestEngine
engine = BacktestEngine(
strategy_class=MyStrategy,
config={...},
start_date="2024-01-01",
end_date="2024-06-01",
initial_capital=10000,
)
results = engine.run()
results.sharpe_ratio
results.max_drawdown
results.total_return
results.plot() # Matplotlib equity curve
backtest pnl, backtest paper, backtest sweep) require an explicit -s strategy_name flag. They do not auto-discover strategies from the current directory like strat run does.total_return_pct and similar _pct result fields are decimal fractions (0.33 = 33%), not percentages.almanak strat new # Interactive strategy scaffolding
almanak strat new -t mean_reversion -n my_rsi -c arbitrum # Non-interactive
almanak strat demo # Browse and copy a working demo strategy
Templates: blank, dynamic_lp, mean_reversion, bollinger, basis_trade, lending_loop, copy_trader
almanak strat run --once # Single iteration (from strategy dir)
almanak strat run -d path/to/strat --once # Explicit directory
almanak strat run --network anvil --once # Local Anvil fork
almanak strat run --interval 30 # Continuous (30s between iterations)
almanak strat run --dry-run --once # No transactions submitted
almanak strat run --fresh --once # Clear state before running
almanak strat run --id abc123 --once # Resume previous run
almanak strat run --dashboard # Launch live monitoring dashboard
almanak strat backtest pnl -s my_strategy # Historical PnL simulation
almanak strat backtest paper -s my_strategy # Paper trading on Anvil fork
almanak strat backtest sweep -s my_strategy # Parameter sweep optimization
almanak strat teardown plan # Preview teardown intents
almanak strat teardown execute # Execute teardown
almanak gateway # Start standalone gateway
almanak gateway --network anvil # Gateway for local Anvil testing
almanak gateway --port 50052 # Custom port
almanak agent install # Auto-detect platforms and install
almanak agent install -p claude # Install for specific platform
almanak agent install -p all # Install for all 9 platforms
almanak agent update # Update installed skill files
almanak agent status # Check installation status
almanak docs path # Path to bundled LLM docs
almanak docs dump # Print full LLM docs
almanak docs agent-skill # Path to bundled agent skill
almanak docs agent-skill --dump # Print agent skill content
| Chain | Enum Value | Config Name |
|---|---|---|
| Ethereum | ETHEREUM | ethereum |
| Arbitrum | ARBITRUM | arbitrum |
| Optimism | OPTIMISM | optimism |
| Base | BASE |
| Protocol | Enum Value | Type | Config Name |
|---|---|---|---|
| Uniswap V3 | UNISWAP_V3 | DEX / LP | uniswap_v3 |
| PancakeSwap V3 | PANCAKESWAP_V3 | DEX / LP | pancakeswap_v3 |
| SushiSwap V3 | SUSHISWAP_V3 | DEX / LP | sushiswap_v3 |
Protocol enum value. Use the string config name (e.g., protocol="aave_v3") in intents. They are resolved by the intent compiler and transaction builder directly.| Network | Enum Value | Description |
|---|---|---|
| Mainnet | MAINNET | Production chains |
| Anvil | ANVIL | Local fork for testing |
| Sepolia | SEPOLIA | Testnet |
GMX V2 (Perpetuals)
"BTC/USD", "ETH/USD", "LINK/USD" (not dash).Intent.perp_open(), the SDK submits an order creation transaction. A GMX keeper then executes the actual position change in a separate transaction. on_intent_executed(success=True) fires when the order creation TX confirms, not when the keeper executes the position. Strategies should poll position state before relying on it.get_all_positions() may not return positions immediately after opening due to keeper delay. Allow a few seconds before querying.def decide(self, market):
rsi = market.rsi(self.base_token, period=self.rsi_period)
quote_bal = market.balance(self.quote_token)
base_bal = market.balance(self.base_token)
if rsi.is_oversold and quote_bal.balance_usd >= self.trade_size:
return Intent.swap(
from_token=self.quote_token, to_token=self.base_token,
amount_usd=self.trade_size, max_slippage=Decimal("0.005"),
)
if rsi.is_overbought and base_bal.balance_usd >= self.trade_size:
return Intent.swap(
from_token=self.base_token, to_token=self.quote_token,
amount_usd=self.trade_size, max_slippage=Decimal("0.005"),
)
return Intent.hold(reason=f"RSI={rsi.value:.1f} in neutral zone")
def decide(self, market):
price = market.price(self.base_token)
position_id = self.state.get("lp_position_id")
if position_id:
# Check if price is out of range - close and reopen
if price < self.state.get("range_lower") or price > self.state.get("range_upper"):
return Intent.lp_close(position_id=position_id, protocol="uniswap_v3")
# Open new position centered on current price
atr = market.atr(self.base_token)
half_range = price * (atr.value_percent / Decimal("100")) * 2 # value_percent is percentage points
return Intent.lp_open(
pool="WETH/USDC",
amount0=Decimal("1"), amount1=Decimal("2000"),
range_lower=price - half_range,
range_upper=price + half_range,
)
def decide(self, market):
return Intent.sequence(
intents=[
Intent.swap(from_token="USDC", to_token="WETH", amount_usd=Decimal("5000")),
Intent.supply(protocol="aave_v3", token="WETH", amount="all"),
Intent.borrow(
protocol="aave_v3",
collateral_token="WETH", collateral_amount=Decimal("0"),
borrow_token="USDC", borrow_amount=Decimal("3000"),
),
],
description="Leverage loop: buy WETH, supply, borrow USDC",
)
from almanak.framework.alerting import AlertManager
class MyStrategy(IntentStrategy):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.alerts = AlertManager.from_config(self.config.get("alerting", {}))
def decide(self, market):
rsi = market.rsi("WETH")
if rsi.value < 20:
self.alerts.send("Extreme oversold: RSI={:.1f}".format(rsi.value), level="warning")
# ... trading logic
Implement teardown so the strategy can cleanly exit positions:
def supports_teardown(self) -> bool:
return True
def generate_teardown_intents(self, mode, market=None) -> list[Intent]:
intents = []
position_id = self.state.get("lp_position_id")
if position_id:
intents.append(Intent.lp_close(position_id=position_id))
# Swap all base token back to quote
intents.append(Intent.swap(
from_token=self.base_token, to_token=self.quote_token,
amount="all", max_slippage=Decimal("0.03"),
))
return intents
Always wrap decide() in try/except and return Intent.hold() on error:
def decide(self, market):
try:
# ... strategy logic
except Exception as e:
logger.exception(f"Error in decide(): {e}")
return Intent.hold(reason=f"Error: {e}")
The framework retries each failed intent up to max_retries (default: 3) with exponential backoff. However, after all retries are exhausted the strategy continues running and will attempt the same trade on the next iteration. Without a circuit breaker, this creates an infinite loop of reverted transactions that burn gas without any hope of success.
Always track consecutive execution failures in persistent state and stop trading (or enter an extended cooldown) after a threshold is reached:
MAX_CONSECUTIVE_FAILURES = 3 # Stop after 3 rounds of failed intents
FAILURE_COOLDOWN_SECONDS = 1800 # 30-min cooldown before retrying
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.consecutive_failures = 0
self.failure_cooldown_until = 0.0
def decide(self, market):
try:
now = time.time()
# Circuit breaker: skip trading while in cooldown
if now < self.failure_cooldown_until:
remaining = int(self.failure_cooldown_until - now)
return Intent.hold(
reason=f"Circuit breaker active, cooldown {remaining}s remaining"
)
# Circuit breaker: enter cooldown after too many failures
if self.consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
self.failure_cooldown_until = now + FAILURE_COOLDOWN_SECONDS
self.consecutive_failures = 0
logger.warning(
f"Circuit breaker tripped after {MAX_CONSECUTIVE_FAILURES} "
f"consecutive failures, cooling down {FAILURE_COOLDOWN_SECONDS}s"
)
return Intent.hold(reason="Circuit breaker tripped")
# ... normal strategy logic ...
except Exception as e:
logger.exception(f"Error in decide(): {e}")
return Intent.hold(reason=f"Error: {e}")
def on_intent_executed(self, intent, success: bool, result):
if success:
self.consecutive_failures = 0 # Reset on success
else:
self.consecutive_failures += 1
logger.warning(
f"Intent failed ({self.consecutive_failures}/{MAX_CONSECUTIVE_FAILURES})"
)
def get_persistent_state(self) -> dict:
return {
"consecutive_failures": self.consecutive_failures,
"failure_cooldown_until": self.failure_cooldown_until,
}
def load_persistent_state(self, state: dict) -> None:
self.consecutive_failures = int(state.get("consecutive_failures", 0))
self.failure_cooldown_until = float(state.get("failure_cooldown_until", 0))
Important: Only update trade-timing state (e.g. last_trade_ts) inside on_intent_executed when success=True, not when the intent is created. Setting it at creation time means a failed trade still resets the interval timer, causing the strategy to wait before retrying — or worse, to keep retrying on a fixed schedule with no failure awareness.
Override on_sadflow_enter to react to specific error types during intent retries. This hook is called before each retry attempt and lets you modify the transaction (e.g. increase gas or slippage) or abort early:
from almanak.framework.intents.state_machine import SadflowAction
class MyStrategy(IntentStrategy):
def on_sadflow_enter(self, error_type, attempt, context):
# Abort immediately on insufficient funds — retrying won't help
if error_type == "INSUFFICIENT_FUNDS":
return SadflowAction.abort("Insufficient funds, stopping retries")
# Increase gas limit for gas-related errors
if error_type == "GAS_ERROR" and context.action_bundle:
modified = self._increase_gas(context.action_bundle)
return SadflowAction.modify(modified, reason="Increased gas limit")
# For slippage errors ("Too little received"), abort after 1 attempt
# since retrying with the same parameters will produce the same result
if error_type == "SLIPPAGE" and attempt >= 1:
return SadflowAction.abort("Slippage error persists, aborting")
# Default: let the framework retry with backoff
return None
Error types passed to on_sadflow_enter (from _categorize_error in state_machine.py):
GAS_ERROR — gas estimation failed or gas limit exceededINSUFFICIENT_FUNDS — wallet balance too lowSLIPPAGE — "Too little received" or similar DEX revertTIMEOUT — transaction confirmation timed outNONCE_ERROR — nonce mismatch or conflictREVERT — generic transaction revertRATE_LIMIT — RPC or API rate limit hitNETWORK_ERROR — connection or network failureCOMPILATION_PERMANENT — unsupported protocol/chain (non-retriable)Before deploying to mainnet:
--network anvil --once until decide() works correctly--dry-run --once on mainnet to verify compilation without submitting transactionsamount= (token units) for swaps if amount_usd= causes reverts (see swap reference above)get_persistent_state() / load_persistent_state() if your strategy tracks positions or phase state--id resume)| Error | Cause | Fix |
|---|---|---|
TokenNotFoundError | Token symbol not in registry | Use exact symbol (e.g., "WETH" not "ETH" for swaps). Check resolver.resolve("TOKEN", "chain"). |
Gateway not available | Gateway not running | Use almanak strat run (auto-starts gateway) or start manually with almanak gateway. |
ALMANAK_PRIVATE_KEY not set | Missing .env | Add to your file. |
--verbose flag for detailed logging: almanak strat run --once --verbose--dry-run to test decide() without submitting transactions--log-file out.json for machine-readable JSON logsself.state persists between iterationsalmanak strat backtest paper -s my_strategy runs real execution on AnvilWeekly Installs
1
Repository
GitHub Stars
14
First Seen
1 day ago
Security Audits
Gen Agent Trust HubFailSocketPassSnykWarn
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1
AI代理钱包工具 - 多链资产管理、余额查询、转账签名与交易历史
3,800 周安装
AWS SDK Java 2.x KMS 加密指南:密钥管理、信封加密与 Spring Boot 集成
368 周安装
Sentry JavaScript SDK 升级指南:AI引导跨版本迁移,解决弃用API与破坏性变更
379 周安装
NotebookLM 研究助手技能:基于来源的文档查询与智能管理工具
369 周安装
AWS SDK for Java 2.x 核心配置指南:生产环境安全设置与最佳实践
371 周安装
Kaizen持续改进方法:软件开发中的渐进式优化与防错设计实践
373 周安装
AWS SDK Java 2.x Amazon Bedrock 开发指南:集成Claude、Llama、Titan AI模型
371 周安装
base |
| Avalanche | AVALANCHE | avalanche |
| Polygon | POLYGON | polygon |
| BSC | BSC | bsc |
| Sonic | SONIC | sonic |
| Plasma | PLASMA | plasma |
| Blast | BLAST | blast |
| Mantle | MANTLE | mantle |
| Berachain | BERACHAIN | berachain |
| Monad | MONAD | monad |
| TraderJoe V2 | TRADERJOE_V2 | DEX / LP | traderjoe_v2 |
| Aerodrome | AERODROME | DEX / LP | aerodrome |
| Enso | ENSO | Aggregator | enso |
| Pendle | PENDLE | Yield | pendle |
| MetaMorpho | METAMORPHO | Lending | metamorpho |
| LiFi | LIFI | Bridge | lifi |
| Vault | VAULT | ERC-4626 | vault |
| Curve | CURVE | DEX / LP | curve |
| Balancer | BALANCER | DEX / LP | balancer |
| Aave V3 | * | Lending | aave_v3 |
| Morpho Blue | * | Lending | morpho_blue |
| Compound V3 | * | Lending | compound_v3 |
| GMX V2 | * | Perps | gmx_v2 |
| Hyperliquid | * | Perps | hyperliquid |
| Polymarket | * | Prediction | polymarket |
| Kraken | * | CEX | kraken |
| Lido | * | Staking | lido |
| Lagoon | * | Vault | lagoon |
None — unclassified errorALMANAK_PRIVATE_KEY=0x....envAnvil not found | Foundry not installed | Install: `curl -L https://foundry.paradigm.xyz |
RSI data unavailable | Insufficient price history | The gateway needs time to accumulate data. Try a longer timeframe or wait. |
Insufficient balance | Wallet doesn't have enough tokens | For Anvil: add anvil_funding to config.json. For mainnet: fund the wallet. |
Slippage exceeded | Trade too large or pool illiquid | Increase max_slippage or reduce trade size. |
Too little received (repeated reverts) | Placeholder prices used for slippage calculation, or stale price data | Ensure real price feeds are active (not placeholder). Implement on_sadflow_enter to abort on persistent slippage errors. Add a circuit breaker to stop retrying the same failing trade. |
| Transactions keep reverting after max retries | Strategy re-emits the same failing intent on subsequent iterations | Track consecutive_failures in persistent state and enter cooldown after a threshold. See the "Execution Failure Tracking" pattern. |
| Gas wasted on reverted transactions | No circuit breaker; framework retries 3x per intent, then strategy retries next iteration indefinitely | Implement on_intent_executed callback to count failures and on_sadflow_enter to abort non-recoverable errors early. |
| Intent compilation fails | Wrong parameter types | Ensure amounts are Decimal, not float. Use Decimal(str(value)). |