Skip to content

Commit 769f07b

Browse files
committed
v5.0.6
Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
1 parent 71e8424 commit 769f07b

15 files changed

Lines changed: 542 additions & 15 deletions

‎backend_api_python/app/routes/agent_v1/strategy_sources.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any
55

66
from app.services.script_source import get_script_source_service
7+
from app.services.strategy_authoring import get_strategy_authoring_contract
78
from app.services.strategy_v2 import canonical_source_metadata, compile_strategy_v2
89
from app.utils.agent_auth import SCOPE_R, SCOPE_W, agent_required, current_user_id
910
from app.utils.logger import get_logger
@@ -30,6 +31,13 @@
3031
)
3132

3233

34+
@agent_v1_bp.route("/strategy-sources/authoring-contract", methods=["GET"])
35+
@agent_required(SCOPE_R)
36+
def strategy_authoring_contract():
37+
"""Return the canonical Strategy API V2 contract and starter source."""
38+
return envelope(get_strategy_authoring_contract())
39+
40+
3341
def _is_hidden_source(item: dict | None) -> bool:
3442
if not isinstance(item, dict):
3543
return False

‎backend_api_python/app/routes/ai_chat.py‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,10 @@ def _fallback_agent_intent(
214214
"entities": {
215215
"symbol": selected_symbol,
216216
"market": (context or {}).get("market") or (context or {}).get("resolved_market") or "",
217-
"timeframe": "",
217+
"timeframe": (context or {}).get("timeframe") or "",
218+
"exchange_id": (context or {}).get("exchange_id") or (context or {}).get("exchangeId") or "",
219+
"market_type": (context or {}).get("market_type") or (context or {}).get("marketType") or "",
220+
"instrument_id": (context or {}).get("instrument_id") or (context or {}).get("instrumentId") or "",
218221
"strategy_template": "",
219222
},
220223
"skills": [skill.to_public(language) for skill in match_skills(message, base_intent, limit=5)],
@@ -254,7 +257,10 @@ def _normalize_agent_intent(raw: dict, message: str, has_image: bool, context: d
254257
entities = {
255258
"symbol": str(entities.get("symbol") or selected_symbol or "").strip(),
256259
"market": str(entities.get("market") or selected_market or "").strip(),
257-
"timeframe": str(entities.get("timeframe") or "").strip(),
260+
"timeframe": str(entities.get("timeframe") or context.get("timeframe") or "").strip(),
261+
"exchange_id": str(entities.get("exchange_id") or context.get("exchange_id") or context.get("exchangeId") or "").strip(),
262+
"market_type": str(entities.get("market_type") or context.get("market_type") or context.get("marketType") or "").strip(),
263+
"instrument_id": str(entities.get("instrument_id") or context.get("instrument_id") or context.get("instrumentId") or "").strip(),
258264
"strategy_template": str(entities.get("strategy_template") or "").strip(),
259265
"asset_class": str(entities.get("asset_class") or "").strip(),
260266
}
@@ -307,6 +313,7 @@ def _classify_agent_intent(message: str, attachments: list[dict], context: dict,
307313
"workflow such as indicator creation, strategy creation, backtest, or scheduled analysis. "
308314
"For creation, use indicator_ide only for chart-only indicators and visual overlays. "
309315
"Use script_strategy for executable Strategy API V2 sources, backtestable strategies, live strategies, robots, or template-style requests. "
316+
"Preserve selected timeframe, exchange_id, market_type, and instrument_id in entities. A missing timeframe is not blocking for strategy creation because the source generator chooses a conservative source-owned default. "
310317
"If the user asks to create/build/write/generate a runnable strategy and enough target context "
311318
"is available, set should_execute=true. If required data is missing, list it in required_missing. "
312319
"Support every configured UI language and mixed multilingual prompts."
@@ -323,6 +330,9 @@ def _classify_agent_intent(message: str, attachments: list[dict], context: dict,
323330
"symbol": "",
324331
"market": "",
325332
"timeframe": "",
333+
"exchange_id": "",
334+
"market_type": "",
335+
"instrument_id": "",
326336
"strategy_template": "",
327337
"asset_class": "",
328338
},
@@ -339,6 +349,10 @@ def _classify_agent_intent(message: str, attachments: list[dict], context: dict,
339349
"symbol": context.get("symbol") or context.get("selected_symbol") or "",
340350
"resolved_market": context.get("resolved_market") or "",
341351
"resolved_symbol": context.get("resolved_symbol") or "",
352+
"timeframe": context.get("timeframe") or "",
353+
"exchange_id": context.get("exchange_id") or context.get("exchangeId") or "",
354+
"market_type": context.get("market_type") or context.get("marketType") or "",
355+
"instrument_id": context.get("instrument_id") or context.get("instrumentId") or "",
342356
},
343357
"available_intents": [
344358
"general", "market_analysis", "chart_image_analysis", "strategy_build",

‎backend_api_python/app/services/ai_generation_contracts.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,16 @@
2727
- Store per-run state on the global `g` namespace.
2828
- Confirm decisions from visible completed data only. Never read future rows, use negative shifts, or otherwise introduce look-ahead bias.
2929
30+
## Parameters and metadata
31+
- Declare tunable strategy knobs with `# @param <name> <int|float|bool|str> <default> <description>` and keep every declared default identical to the fallback used in code.
32+
- Read run-supplied values only inside executable handlers or scheduled callbacks with `context.params.get("name", same_default)`. The discovery context used by `initialize(context)` has no `params`; never read `context.params` in `initialize`.
33+
- Parameters may control signal periods, thresholds, target weights, stops, take profit, trailing protection, cooldowns, and bounded layer counts.
34+
- Do not disguise universe, symbol, market type, frequency, leverage permission, initial capital, date range, commission, or slippage as ordinary strategy parameters.
35+
- Use `context.set_metadata(...)` in `initialize` for stable descriptive metadata such as direction mode and strategy family. Metadata is not a substitute for executable risk logic.
36+
3037
## Data and factors
3138
- Historical-bar signatures are exact: `get_history(count, frequency=None, field=None, security_list=None)` and `data.history(symbols, count, fields=None)`.
39+
- Read the current scalar field with `data.current(symbol, field="close")`. There is no `get_current_data` API and `data.current(...)` does not return an object with a `.close` attribute.
3240
- In `get_history(...)`, `count` is always the first argument and must be an integer. In `data.history(...)`, symbols are first and the integer count is second. Prefer explicit keywords when using `data.history`, for example `data.history(symbol, count=60, fields=["close"])`.
3341
- A history request for one symbol returns a pandas `DataFrame` directly. Use `bars["close"]`; never index the result again with `bars[symbol]`. Multiple-symbol requests return a dictionary keyed by canonical symbol.
3442
- Use `indicator(name, symbol, **params)`, `factor(name, symbol, **params)`, or `get_factors(symbols, names, **params)` for technical factors.
@@ -41,6 +49,7 @@
4149
- Order-helper signatures are exact: `order(symbol, amount)`, `order_value(symbol, value)`, `order_target(symbol, amount)`, `order_target_value(symbol, value)`, and `order_target_percent(symbol, percent)`.
4250
- These are runtime-bound global helpers. Never pass `context` as their first argument. Optional execution and protection values must be keyword arguments after the two required arguments.
4351
- `get_position(symbol)` returns a `Position` object. Read `position.amount`, `position.avg_cost`, and `position.last_price` directly; never use dictionary membership, subscripting, `.get(...)`, or `getattr(...)` on it.
52+
- A `Position` has no `.quantity` or `.cost_basis`; use `.amount` and `.avg_cost`.
4453
- Use `get_positions(...)` when a dictionary of multiple positions is required.
4554
- Values passed to value-based order APIs are quote-currency exposure targets. Keep sizing bounded by available capital and explicit allocation rules.
4655
- Keep long entry, long exit, short entry, and short exit conditions independent. A bearish long exit is not automatically a short entry.
@@ -72,12 +81,16 @@
7281
- Require a metadata docstring and `initialize(context)`.
7382
- Require a source-owned universe and subscription.
7483
- Require at least one executable handler or registered schedule callback.
84+
- Declare tunable knobs with `# @param` and read them with matching `context.params.get(...)` fallbacks only inside executable handlers or callbacks. Never read `context.params` in `initialize`.
85+
- Do not expose universe, symbol, market type, frequency, leverage permission, initial capital, date range, commission, or slippage as ordinary strategy parameters.
7586
- Use only Strategy API V2 data, factor, fundamental, position, and order APIs.
7687
- Prefer `handle_data(context, data)` for single-symbol signal strategies. Use schedules only for an explicitly requested schedule or periodic portfolio rebalance.
7788
- Schedule helpers are global calls: `run_daily(callback, time="HH:MM")`, `run_weekly(callback, weekday=1, time="HH:MM")`, and `run_monthly(callback, monthday=1, time="HH:MM")`. Never call them through `context`.
7889
- Enforce exact history signatures: `get_history(count, frequency, field, security_list)` and `data.history(symbols, count, fields)`. A single-symbol result is already a DataFrame.
90+
- Use `data.current(symbol, field="close")` for a current scalar field. Replace every `get_current_data` call; that API does not exist.
7991
- Enforce exact order signatures such as `order_target_percent(symbol, percent)` and never pass `context` to a global order helper.
8092
- Treat `get_position(symbol)` as a `Position` object with direct `.amount`, `.avg_cost`, and `.last_price` attributes. Never treat it as a dictionary or use `getattr`.
93+
- Replace legacy `.quantity` and `.cost_basis` position access with `.amount` and `.avg_cost`.
8194
- Preserve completed-data-only execution and remove look-ahead.
8295
- Keep symbol, market, frequency, schedule, and universe in source code.
8396
- Permit user-adjustable leverage only for Crypto `@swap` instruments and only after `context.allow_leverage(max_leverage=N)`.

‎backend_api_python/app/services/ai_tool_registry.py‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,18 @@ def prompt_line(self, language: str) -> str:
321321
priority=86,
322322
safety="Do not use this as a live-start path.",
323323
),
324+
ToolDefinition(
325+
id="mcp.get_strategy_authoring_contract",
326+
category="strategy",
327+
label_zh="读取 Strategy API V2 开发契约",
328+
label_en="Get Strategy API V2 authoring contract",
329+
description_zh="读取策略源码归属、运行时 API、禁止项和 starter template。",
330+
description_en="Read the source-ownership rules, runtime APIs, forbidden patterns, and starter template.",
331+
route="/api/agent/v1/strategy-sources/authoring-contract",
332+
produces=("strategy_contract",),
333+
risk_level="read",
334+
priority=96,
335+
),
324336
ToolDefinition(
325337
id="mcp.list_strategy_templates",
326338
category="strategy",
@@ -619,7 +631,7 @@ def public_tool_registry(language: str = "zh-CN") -> dict[str, Any]:
619631
if tool.enabled:
620632
categories[tool.category] = categories.get(tool.category, 0) + 1
621633
return {
622-
"version": "2026.07.18.1",
634+
"version": "2026.07.20.1",
623635
"total": sum(categories.values()),
624636
"categories": categories,
625637
"tools": list_tools(language),

‎backend_api_python/app/services/llm.py‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -692,8 +692,26 @@ def call_llm_api(self, messages: list, model: str = None, temperature: float = 0
692692
2. Otherwise, use the configured LLM_PROVIDER with normalized model name
693693
3. Fall back to provider's default model if model name is incompatible
694694
"""
695-
# Smart provider detection: if model specifies a provider and we have its API key, use it
696-
if model and not provider:
695+
cfg = load_addon_config()
696+
explicit_provider_name = str(
697+
cfg.get('llm', {}).get('provider') or os.getenv('LLM_PROVIDER', '')
698+
).strip().lower()
699+
explicit_provider = None
700+
if explicit_provider_name:
701+
try:
702+
explicit_provider = LLMProvider(explicit_provider_name)
703+
except ValueError:
704+
explicit_provider = None
705+
override_provider = None
706+
if self._provider_override:
707+
try:
708+
override_provider = LLMProvider(self._provider_override.lower())
709+
except ValueError:
710+
override_provider = None
711+
712+
# Infer a provider from the model only when no provider was selected explicitly.
713+
provider_is_locked = provider is not None or override_provider is not None or explicit_provider is not None
714+
if model and not provider_is_locked:
697715
detected_provider = self._detect_provider_from_model(model)
698716
if detected_provider and detected_provider != LLMProvider.OPENROUTER:
699717
# Check if we have API key for the detected provider
@@ -702,14 +720,6 @@ def call_llm_api(self, messages: list, model: str = None, temperature: float = 0
702720
logger.debug(f"Auto-detected provider '{provider.value}' from model '{model}'")
703721

704722
p = provider or self.provider
705-
cfg = load_addon_config()
706-
explicit_provider_name = str(cfg.get('llm', {}).get('provider') or os.getenv('LLM_PROVIDER', '')).strip().lower()
707-
explicit_provider = None
708-
if explicit_provider_name:
709-
try:
710-
explicit_provider = LLMProvider(explicit_provider_name)
711-
except ValueError:
712-
explicit_provider = None
713723
api_key = (self.get_api_key(p) or "").strip()
714724
base_url = (self.get_base_url(p) or "").strip()
715725
if not self.is_configured(p):
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Machine-readable Strategy API V2 authoring contract for external agents."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from app.services.ai_generation_contracts import SCRIPT_STRATEGY_SYSTEM_PROMPT
8+
9+
10+
_STARTER_TEMPLATE = '''"""
11+
Daily Moving Average Regime
12+
Trades a long-only SPY regime from completed daily bars with bounded exposure.
13+
"""
14+
15+
# @param period int 20 Moving-average period range=5:100:5
16+
# @param target_pct float 0.5 Target portfolio weight range=0.1:1.0:0.05
17+
18+
19+
def initialize(context):
20+
context.set_universe(["USStock:SPY"])
21+
context.subscribe(frequency="1d", fields=["close"])
22+
context.set_warmup(22)
23+
context.set_metadata(direction_mode="long_only", strategy_family="trend")
24+
25+
26+
def handle_data(context, data):
27+
period = int(context.params.get("period", 20))
28+
target_pct = float(context.params.get("target_pct", 0.5))
29+
bars = get_history(period + 1, "1d", "close", "USStock:SPY")
30+
if len(bars) < period:
31+
return
32+
close = float(bars["close"].iloc[-1])
33+
average = float(bars["close"].tail(period).mean())
34+
order_target_percent("USStock:SPY", target_pct if close > average else 0.0)
35+
'''
36+
37+
38+
def get_strategy_authoring_contract() -> dict[str, Any]:
39+
"""Return the canonical source-ownership and runtime API contract."""
40+
return {
41+
"version": "strategy-api-v2-source-owned-2026-07",
42+
"doc": "docs/trading/STRATEGY_DEV_GUIDE.md",
43+
"workflow": [
44+
"1. Fetch this contract before generating Strategy API V2 source.",
45+
"2. Generate complete Python source; never send natural language as code.",
46+
"3. Compile with /api/agent/v1/strategy-sources/compile and repair every validation error.",
47+
"4. Save the validated draft with /api/agent/v1/strategy-sources.",
48+
"5. Backtest the saved or validated source before creating a stopped deployment.",
49+
],
50+
"ownership": {
51+
"source": [
52+
"universe",
53+
"market",
54+
"instrument_type",
55+
"frequency",
56+
"subscriptions",
57+
"direction",
58+
"sizing",
59+
"entries",
60+
"exits",
61+
"risk",
62+
"schedules",
63+
],
64+
"run_panel": ["initial_capital", "date_range", "permitted_swap_leverage"],
65+
},
66+
"required": [
67+
"A metadata docstring whose first non-empty line is the strategy name",
68+
"initialize(context) with context.set_universe(...) and context.subscribe(...) calls",
69+
"At least one executable handler or registered schedule callback",
70+
"Canonical instruments such as Crypto:SOL/USDT@spot or USStock:SPY",
71+
],
72+
"forbidden": [
73+
"get_current_data; use data.current(symbol, field='close')",
74+
"Position.quantity or Position.cost_basis; use amount and avg_cost",
75+
"context.run_daily/context.run_weekly/context.run_monthly; schedule helpers are global",
76+
"context.params reads inside initialize(context)",
77+
"Run-panel overrides for symbol, market type, frequency, or leverage permission",
78+
"File, network, process, reflection, or dynamic execution APIs",
79+
],
80+
"system_contract": SCRIPT_STRATEGY_SYSTEM_PROMPT,
81+
"starter_template": _STARTER_TEMPLATE,
82+
}

0 commit comments

Comments
 (0)