Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 1 addition & 13 deletions python/valuecell/agents/common/trading/data/market.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
MarketSnapShotType,
)
from valuecell.agents.common.trading.utils import get_exchange_cls, normalize_symbol
from valuecell.utils.i18n_utils import detect_user_region

from .interfaces import BaseMarketDataSource

Expand All @@ -26,18 +25,7 @@ class SimpleMarketDataSource(BaseMarketDataSource):

def __init__(self, exchange_id: Optional[str] = None) -> None:
if not exchange_id:
# Auto-detect region and select appropriate exchange
region = detect_user_region()
if region == "us":
# Use OKX for United States users (best support for USDT perpetuals)
self._exchange_id = "okx"
logger.info(
"Detected US region, using okx exchange (USDT perpetuals supported)"
)
else:
# Use regular Binance for other regions
self._exchange_id = "binance"
logger.info("Detected non-US region, using binance exchange")
self._exchange_id = "okx"
else:
self._exchange_id = exchange_id

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
from __future__ import annotations

import json
from typing import Optional

from agno.agent import Agent as AgnoAgent
from loguru import logger

from valuecell.agents.common.trading.constants import (
FEATURE_GROUP_BY_KEY,
FEATURE_GROUP_BY_MARKET_SNAPSHOT,
)
from valuecell.utils import model as model_utils

from ...models import ComposeContext, GridParamAdvice, UserRequest

SYSTEM_PROMPT = (
"You are a grid parameter advisor. "
"Given the current market snapshot metrics and runtime settings, propose grid parameters dynamically. "
"Use higher sensitivity (smaller step_pct, larger max_steps) for high-liquidity, high-volatility pairs; lower sensitivity otherwise. "
"Respect typical ranges: step_pct 0.0005~0.01, max_steps 1~5, base_fraction 0.03~0.10. "
"Optionally include grid zone bounds (grid_lower_pct, grid_upper_pct) and grid_count when appropriate. "
"Calibrate base_fraction and optional grid_count using portfolio context: equity, buying_power, free_cash, and constraints.max_leverage. "
"Align parameter sensitivity with available capital and risk limits (cap_factor). Prefer smaller base_fraction and fewer steps when capital is tight. "
"Output pure JSON with fields: grid_step_pct, grid_max_steps, grid_base_fraction, and optionally grid_lower_pct, grid_upper_pct, grid_count, advisor_rationale. "
"advisor_rationale should briefly explain your thinking and operational basis (e.g., volatility, liquidity, funding, OI, buying_power) for parameter selection."
)


class GridParamAdvisor:
def __init__(
self, request: UserRequest, prev_params: Optional[dict] = None
) -> None:
self._request = request
# Previous applied grid params from composer (optional), used to anchor suggestions
self._prev_params = prev_params or {}

async def advise(self, context: ComposeContext) -> Optional[GridParamAdvice]:
cfg = self._request.llm_model_config
try:
model = model_utils.create_model_with_provider(
provider=cfg.provider,
model_id=cfg.model_id,
api_key=cfg.api_key,
)

# Extract a compact per-symbol snapshot of key metrics
keys = (
"price.last",
"price.change_pct",
"price.volume",
"open_interest",
"funding.rate",
)
metrics: dict[str, dict[str, float]] = {}
for fv in context.features or []:
try:
symbol = str(getattr(fv.instrument, "symbol", ""))
meta = fv.meta or {}
if (
meta.get(FEATURE_GROUP_BY_KEY)
!= FEATURE_GROUP_BY_MARKET_SNAPSHOT
):
continue
if symbol not in (self._request.trading_config.symbols or []):
continue
snap = metrics.setdefault(symbol, {})
for k in keys:
val = fv.values.get(k)
if val is not None:
try:
snap[k] = float(val) # type: ignore
except Exception:
pass
except Exception:
continue

payload = {
"market_type": self._request.exchange_config.market_type,
"decide_interval": self._request.trading_config.decide_interval,
"symbols": self._request.trading_config.symbols,
"snapshot_metrics": metrics,
}

# Include previous applied parameters to promote continuity and gradual changes
try:
prev = {}
for k in (
"grid_step_pct",
"grid_max_steps",
"grid_base_fraction",
"grid_lower_pct",
"grid_upper_pct",
"grid_count",
):
v = self._prev_params.get(k)
if v is not None:
prev[k] = float(v) if isinstance(v, (int, float)) else v
if prev:
payload["previous_params"] = prev
except Exception:
# Ignore if previous params cannot be assembled
pass

# Include portfolio/buying power context so the model scales params realistically
try:
pv = context.portfolio
# Derive equity with safe fallbacks
equity: Optional[float] = None
try:
if getattr(pv, "total_value", None) is not None:
equity = float(pv.total_value) # type: ignore
else:
bal = float(pv.account_balance) # type: ignore
upnl = float(getattr(pv, "total_unrealized_pnl", 0.0) or 0.0) # type: ignore
equity = bal + upnl
except Exception:
equity = None

constraints = getattr(pv, "constraints", None)
max_lev = None
try:
max_lev = (
float(getattr(constraints, "max_leverage", None))
if constraints is not None
and getattr(constraints, "max_leverage", None) is not None
else float(self._request.trading_config.max_leverage)
)
except Exception:
max_lev = None

portfolio_ctx = {
"equity": equity,
"buying_power": getattr(pv, "buying_power", None),
"free_cash": getattr(pv, "free_cash", None),
"constraints": {
"max_leverage": max_lev,
"quantity_step": getattr(constraints, "quantity_step", None)
if constraints
else None,
"min_trade_qty": getattr(constraints, "min_trade_qty", None)
if constraints
else None,
"max_order_qty": getattr(constraints, "max_order_qty", None)
if constraints
else None,
"max_position_qty": getattr(
constraints, "max_position_qty", None
)
if constraints
else None,
},
"cap_factor": float(self._request.trading_config.cap_factor),
}
payload["portfolio"] = portfolio_ctx
except Exception:
# Portfolio context is optional; proceed without if assembly fails
pass

instructions = (
"Return JSON only. Include advisor_rationale summarizing your thought process and operational basis. "
"Keep within ranges; favor smaller step_pct for high-liquidity and high-volatility pairs. "
"If funding.rate is high or open_interest large, prefer tighter grid and smaller base_fraction; otherwise be conservative. "
"Consider portfolio.equity, buying_power, free_cash, constraints.max_leverage, and cap_factor to scale base_fraction and optional grid_count. "
"Avoid suggesting parameter combinations that imply excessive total size under available buying_power. "
"Anchor suggestions to previous_params when provided; prefer gradual adjustments (e.g., limit grid_count delta within ±2 and keep step_pct changes small) unless metrics indicate a clear regime shift."
)
prompt = (
f"{instructions}\n\nContext:\n{json.dumps(payload, ensure_ascii=False)}"
)

agent = AgnoAgent(
model=model,
output_schema=GridParamAdvice,
markdown=False,
instructions=[SYSTEM_PROMPT],
use_json_mode=model_utils.model_should_use_json_mode(model),
)

response = await agent.arun(prompt)
content = getattr(response, "content", None) or response
if isinstance(content, GridParamAdvice):
logger.info(
"LLM grid advice: step_pct={}, max_steps={}, base_fraction={}, lower={}, upper={}, count={}, rationale={}",
content.grid_step_pct,
content.grid_max_steps,
content.grid_base_fraction,
content.grid_lower_pct,
content.grid_upper_pct,
content.grid_count,
getattr(content, "advisor_rationale", None),
)
return content
logger.warning("LLM advice failed validation: {}", content)
except Exception as exc:
logger.error("LLM param advisor error: {}", exc)
return None
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ def _build_llm_prompt(self, context: ComposeContext) -> str:
"features.1m = structural trends (240 periods), features.1s = realtime signals (180 periods). "
"market.funding_rate: positive = longs pay shorts. "
"Respect constraints and risk_flags. Prefer NOOP when edge unclear. "
"Always include a concise top-level 'rationale'. "
"If you choose NOOP (items is empty), set 'rationale' to explain why: reference current prices and 'price.change_pct' vs thresholds, and any constraints or risk flags that led to NOOP. "
"Output JSON with items array."
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
- Consider existing position entry times when deciding new actions. Use each position's `entry_ts` (entry timestamp) as a signal: avoid opening, flipping, or repeatedly scaling the same instrument shortly after its entry unless the new signal is strong (confidence near 1.0) and constraints allow it.
- Treat recent entries as a deterrent to new opens to reduce churn — do not re-enter or flip a position within a short holding window unless there is a clear, high-confidence reason. This rule supplements Sharpe-based and other risk heuristics to prevent overtrading.

OUTPUT & EXPLANATION
- Always include a brief top-level rationale summarizing your decision basis.
- Your rationale must transparently reveal your thinking process (signals evaluated, thresholds, trade-offs) and the operational steps (how sizing is derived, which constraints/normalization will be applied).
- If no actions are emitted (noop), your rationale must explain specific reasons: reference current prices and price.change_pct relative to your thresholds, and note any constraints or risk flags that caused noop.

MARKET FEATURES
The Context includes `features.market_snapshot`: a compact, per-cycle bundle of references derived from the latest exchange snapshot. Each item corresponds to a tradable symbol and may include:

Expand Down
24 changes: 24 additions & 0 deletions python/valuecell/agents/common/trading/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ class TradingConfig(BaseModel):
description="Notional cap factor used by the composer to limit per-symbol exposure (e.g., 1.5)",
gt=0,
)
# Grid parameters are model-decided at runtime; no user-configurable grid_* fields.

@field_validator("symbols")
@classmethod
Expand Down Expand Up @@ -339,6 +340,29 @@ class Candle(BaseModel):
interval: str = Field(..., description='Interval string, e.g., "1m", "5m"')


class GridParamAdvice(BaseModel):
"""LLM-advised grid parameter set.

Advisor should return sensible values within typical ranges:
- grid_step_pct: 0.0005 ~ 0.01
- grid_max_steps: 1 ~ 5
- grid_base_fraction: 0.03 ~ 0.10
"""

ts: int = Field(..., description="Advice timestamp in ms")
grid_step_pct: float = Field(..., gt=0)
grid_max_steps: int = Field(..., gt=0)
grid_base_fraction: float = Field(..., gt=0)
# Optional zone and discretization
grid_lower_pct: Optional[float] = Field(default=None, gt=0)
grid_upper_pct: Optional[float] = Field(default=None, gt=0)
grid_count: Optional[int] = Field(default=None, gt=0)
advisor_rationale: Optional[str] = Field(
default=None,
description="Model-provided reasoning explaining how grid parameters were chosen",
)


CommonKeyType = str
CommonValueType = float | str | int

Expand Down
7 changes: 4 additions & 3 deletions python/valuecell/agents/grid_agent/grid_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ async def _create_decision_composer(
# Adjust step_pct / max_steps / base_fraction as needed
return GridComposer(
request=request,
step_pct=0.005, # ~0.5% per step
max_steps=3, # up to 3 steps per cycle
base_fraction=0.08, # base order size = equity * 8%
step_pct=0.001, # 0.1% per step (more sensitive)
max_steps=3,
base_fraction=0.08,
use_llm_params=True,
)