refactor!: 删除可执行价位能力,不再以配置开关形式保留

BREAKING CHANGE: 上一提交引入的 enable_execution_levels 开关一并移除。

改为直接删除而非默认关闭:荐股软件的认定看软件是否「具备」该功能,
留一个开关在代码里、README 还写着怎么打开,那软件依然具备该功能。
删掉才是真的不具备,同时少一个开关、少两个 schema 变体、少两处分支。

- 删除 TraderProposalWithLevels / PortfolioDecisionWithTarget 两个变体
  与 trader_proposal_model() / portfolio_decision_model() 选择器
- 删除 entry_price / stop_loss / position_sizing / price_target 字段
- 渲染函数不再输出对应四节,getattr 兼容层一并移除
- create_trader / create_portfolio_manager / GraphSetup 去掉开关参数
- default_config 去掉 enable_execution_levels
- 提示词保持收紧(仅删字段挡不住模型写进散文字段)
- 测试 TestExecutionLevelsFlag → TestNoExecutionLevels:
  锁定「schema 无价位字段 / 提示词禁止 / 渲染永不输出」

需要该能力的使用者可自行 fork 添加(Apache-2.0 允许)。

测试:161 passed + 48 subtests passed。
This commit is contained in:
Simon Lin 2026-07-24 19:54:24 +12:00
parent 35a297c3ab
commit d55820c08d
10 changed files with 55 additions and 188 deletions

View File

@ -8,14 +8,15 @@ Breaking changes within the 0.x line are called out explicitly.
## [0.3.0] — 2026-07-24
明确项目定位为「框架的工程实现与研究复现」,并把可执行价位改为默认关闭。**有破坏性变更**(见下)。
明确项目定位为「框架的工程实现与研究复现」,并**移除可执行价位相关能力**。**有破坏性变更**(见下)。
### 变更(破坏性)
- **`enable_execution_levels` 新增,默认 `False`**:默认情况下 Trader 与 Portfolio Manager 只输出方向 / 评级与理由,**不再产出建仓价、止损位、仓位、目标价**。
- Schema 拆分:`TraderProposal`(默认,无价位)/ `TraderProposalWithLevels`opt-in`PortfolioDecision`(默认,无目标价)/ `PortfolioDecisionWithTarget`opt-in。用 `trader_proposal_model()` / `portfolio_decision_model()` 按开关取用。
- 提示词同步收紧:光删字段挡不住模型把价位写进散文字段,因此系统提示与 `executive_summary` / `reasoning` 的字段描述都显式要求不给价位。
- 渲染函数改用 `getattr`,两种变体都能渲染,下游 markdown 格式不变。
- **升级影响**:依赖 `TraderProposal.entry_price` 等字段的下游代码,需改用 `*WithLevels` 变体,或在 config 里设 `enable_execution_levels: True`
### 移除(破坏性)
- **可执行价位能力整体删除**Trader 与 Portfolio Manager 现在只输出方向 / 评级与理由,框架内**不再存在**建仓价、止损位、仓位、目标价这类输出。
- 删除字段:`TraderProposal.entry_price` / `.stop_loss` / `.position_sizing``PortfolioDecision.price_target`
- 提示词同步收紧:仅删字段挡不住模型把价位写进散文字段,因此系统提示与 `executive_summary` / `reasoning` 的字段描述都显式要求不给价位。
- 渲染函数不再输出 `**Entry Price**` / `**Stop Loss**` / `**Position Sizing**` / `**Price Target**` 四节;其余 markdown 格式不变。
- **这是删除而不是开关**——不提供 opt-in 配置项。需要这类能力的使用者可自行 fork 添加Apache-2.0 允许),并自行承担相应责任。
- **升级影响**:依赖 `TraderProposal.entry_price` 等字段的下游代码需自行调整。
- **`ResearchPlan.strategic_actions`** 的字段描述去掉「including position sizing guidance」。
### 移除

View File

@ -288,7 +288,6 @@ streamlit run web/app.py
| `backend_url` | `None` | 自定义 API 端点 / 第三方中转网关。可在 Web UI 侧边栏填写,或用 `.env``BACKEND_URL`;方便国内通过代理访问 Claude / OpenAI |
| `output_language` | `"Chinese"` | 报告输出语言(内部辩论始终英文) |
| `market_lookback_days` | `None` | 技术分析回溯天数(分析区间 = 起始日期 → 分析日期。Web/CLI 由「数据起始日期」自动算出;`None` = 模型自选(约 30 天)。#16 |
| `enable_execution_levels` | `False` | **默认关闭。** 关闭时 Trader / Portfolio Manager 只给方向与理由,不产出建仓价 / 止损位 / 仓位 / 目标价。开启后才输出这些可执行价位——是否开启由使用者自行决定并自担责任,详见[项目定位](#项目定位)。 |
| `max_debate_rounds` | `1` | Bull vs Bear 辩论轮数 |
| `max_risk_discuss_rounds` | `1` | 风险三方辩论轮数 |
| `data_vendors` | 全部 `"a_stock"` | 数据供应商路由 |
@ -411,7 +410,7 @@ TradingAgents-Astock/
- **它是什么**[TradingAgents 论文](https://arxiv.org/abs/2412.20138)TauricResearch多 Agent 架构的 A 股工程实现,用于研究与教学——研究多 Agent 辩论在金融文本上的行为、A 股数据源如何接入、结构化输出如何落地。
- **它不是什么**:不是投资顾问、不是荐股软件、不提供任何投资服务。本仓库不发布针对具体证券的分析报告、评级或买卖建议;`examples/` 下只有可自行运行的脚本,没有任何预生成的个股结论。
- **模型和数据都是你自己的**:你配置自己的 LLM API key在自己的机器上运行产出的内容归你所有、由你判断、由你负责。项目本身不托管服务、不代为分析、不接触你的运行结果。
- **默认不产出可执行价位**`enable_execution_levels` 默认为 `False`Trader 与 Portfolio Manager 只给方向与理由,不给建仓价 / 止损位 / 仓位 / 目标价。要打开是使用者的决定,使用者需自行承担相应责任、并自行确认所在司法辖区的资质要求。
- **不产出可执行价位**:框架内**没有**建仓价 / 止损位 / 仓位 / 目标价这类输出——不是默认关闭是代码里就没有。Trader 与 Portfolio Manager 只给方向、评级与理由。需要这类能力的使用者可以自行 fork 添加Apache-2.0 允许),并自行承担相应责任、自行确认所在司法辖区的资质要求。
> **⚠️ 免责声明**
>

View File

@ -5,12 +5,7 @@ import pandas as pd
from unittest.mock import MagicMock, patch
from tradingagents.agents.utils.memory import TradingMemoryLog
from tradingagents.agents.schemas import (
PortfolioDecision,
PortfolioDecisionWithTarget,
PortfolioRating,
portfolio_decision_model,
)
from tradingagents.agents.schemas import PortfolioDecision, PortfolioRating
from tradingagents.graph.reflection import Reflector
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.graph.propagation import Propagator
@ -617,21 +612,21 @@ class TestPortfolioManagerInjection:
downstream consumers (memory log, signal processor, CLI display)
can parse without any extra LLM call."""
captured = {}
decision = PortfolioDecisionWithTarget(
decision = PortfolioDecision(
rating=PortfolioRating.OVERWEIGHT,
executive_summary="Build position gradually over the next two weeks.",
investment_thesis="AI capex cycle remains intact; institutional flows constructive.",
price_target=215.0,
time_horizon="3-6 months",
)
llm = _structured_pm_llm(captured, decision)
pm_node = create_portfolio_manager(llm, enable_execution_levels=True)
pm_node = create_portfolio_manager(llm)
result = pm_node(_make_pm_state())
md = result["final_trade_decision"]
assert "**Rating**: Overweight" in md
assert "**Executive Summary**: Build position gradually" in md
assert "**Investment Thesis**: AI capex cycle" in md
assert "**Price Target**: 215.0" in md
# 框架不产出目标价——渲染里永远不该出现这一节。
assert "Price Target" not in md
assert "**Time Horizon**: 3-6 months" in md
def test_pm_falls_back_to_freetext_when_structured_unavailable(self):

View File

@ -17,10 +17,8 @@ from tradingagents.agents.schemas import (
ResearchPlan,
TraderAction,
TraderProposal,
TraderProposalWithLevels,
render_research_plan,
render_trader_proposal,
trader_proposal_model,
)
from tradingagents.agents.trader.trader import create_trader
@ -41,21 +39,6 @@ class TestRenderTraderProposal:
# analyst stop-signal text and any external code that greps for it.
assert "FINAL TRANSACTION PROPOSAL: **HOLD**" in md
def test_optional_fields_included_when_present(self):
p = TraderProposalWithLevels(
action=TraderAction.BUY,
reasoning="Strong technicals + fundamentals.",
entry_price=189.5,
stop_loss=178.0,
position_sizing="6% of portfolio",
)
md = render_trader_proposal(p)
assert "**Action**: Buy" in md
assert "**Entry Price**: 189.5" in md
assert "**Stop Loss**: 178.0" in md
assert "**Position Sizing**: 6% of portfolio" in md
assert "FINAL TRANSACTION PROPOSAL: **BUY**" in md
def test_optional_fields_omitted_when_absent(self):
p = TraderProposal(action=TraderAction.SELL, reasoning="Guidance cut.")
md = render_trader_proposal(p)
@ -120,36 +103,24 @@ def _structured_trader_llm(captured: dict, proposal: TraderProposal | None = Non
@pytest.mark.unit
class TestExecutionLevelsFlag:
"""Execution levels (entry / stop-loss / sizing) are opt-in, off by default."""
class TestNoExecutionLevels:
"""The framework ships no executable price levels at all — not a default,
not a flag. See TraderProposal's docstring for why."""
def test_default_schema_has_no_level_fields(self):
fields = trader_proposal_model().model_fields
def test_schema_has_no_level_fields(self):
fields = TraderProposal.model_fields
assert "action" in fields and "reasoning" in fields
for f in ("entry_price", "stop_loss", "position_sizing"):
assert f not in fields
def test_enabled_schema_has_level_fields(self):
fields = trader_proposal_model(True).model_fields
for f in ("entry_price", "stop_loss", "position_sizing"):
assert f in fields
def test_default_prompt_forbids_levels(self):
def test_prompt_forbids_levels(self):
captured = {}
trader = create_trader(_structured_trader_llm(captured))
trader(_make_trader_state())
system = next(m["content"] for m in captured["prompt"] if m["role"] == "system")
assert "Do NOT state entry prices" in system
assert "Be specific about entry price" not in system
def test_enabled_prompt_asks_for_levels(self):
captured = {}
trader = create_trader(_structured_trader_llm(captured), enable_execution_levels=True)
trader(_make_trader_state())
system = next(m["content"] for m in captured["prompt"] if m["role"] == "system")
assert "Be specific about entry price" in system
def test_render_omits_levels_for_default_proposal(self):
def test_render_never_emits_levels(self):
p = TraderProposal(action=TraderAction.BUY, reasoning="Trend intact.")
md = render_trader_proposal(p)
for label in ("Entry Price", "Stop Loss", "Position Sizing"):
@ -160,19 +131,16 @@ class TestExecutionLevelsFlag:
class TestTraderAgent:
def test_structured_path_produces_rendered_markdown(self):
captured = {}
proposal = TraderProposalWithLevels(
proposal = TraderProposal(
action=TraderAction.BUY,
reasoning="AI capex cycle intact; institutional flows constructive.",
entry_price=189.5,
stop_loss=178.0,
position_sizing="6% of portfolio",
)
llm = _structured_trader_llm(captured, proposal)
trader = create_trader(llm, enable_execution_levels=True)
trader = create_trader(llm)
result = trader(_make_trader_state())
plan = result["trader_investment_plan"]
assert "**Action**: Buy" in plan
assert "**Entry Price**: 189.5" in plan
assert "**Reasoning**: AI capex cycle intact" in plan
assert "FINAL TRANSACTION PROPOSAL: **BUY**" in plan
# The same rendered markdown is also added to messages for downstream agents.
assert plan in result["messages"][0].content

View File

@ -10,7 +10,7 @@ back gracefully to free-text generation.
from __future__ import annotations
from tradingagents.agents.schemas import portfolio_decision_model, render_pm_decision
from tradingagents.agents.schemas import PortfolioDecision, render_pm_decision
from tradingagents.agents.utils.agent_utils import (
build_instrument_context,
get_language_instruction,
@ -21,20 +21,16 @@ from tradingagents.agents.utils.structured import (
)
def create_portfolio_manager(llm, enable_execution_levels: bool = False):
structured_llm = bind_structured(
llm, portfolio_decision_model(enable_execution_levels), "Portfolio Manager"
)
# Mirrors the Trader: the schema alone cannot stop the model from putting
# levels into the prose fields, so say it in the prompt too.
levels_rule = (
""
if enable_execution_levels
else (
"\n- Do NOT state entry prices, stop-loss levels, target prices or "
"position sizes for this security; give the rating and the reasoning."
)
)
# Mirrors the Trader: the schema alone cannot stop the model from putting
# price levels into the prose fields, so the prompt says it explicitly too.
_NO_LEVELS_RULE = (
"\n- Do NOT state entry prices, stop-loss levels, target prices or "
"position sizes for this security; give the rating and the reasoning."
)
def create_portfolio_manager(llm):
structured_llm = bind_structured(llm, PortfolioDecision, "Portfolio Manager")
def portfolio_manager_node(state) -> dict:
instrument_context = build_instrument_context(state["company_of_interest"])
@ -83,7 +79,7 @@ def create_portfolio_manager(llm, enable_execution_levels: bool = False):
---
Be decisive and ground every conclusion in specific evidence from the analysts.{levels_rule}{get_language_instruction()}"""
Be decisive and ground every conclusion in specific evidence from the analysts.{_NO_LEVELS_RULE}{get_language_instruction()}"""
final_trade_decision = invoke_structured_or_freetext(
structured_llm,

View File

@ -112,10 +112,12 @@ class TraderProposal(BaseModel):
The trader reads the Research Manager's investment plan and the analyst
reports, then states a direction and the reasoning behind it.
This default variant deliberately carries **no executable price levels**
(entry / stop-loss / sizing). Those live in
:class:`TraderProposalWithLevels`, which is opt-in via the
``enable_execution_levels`` config flag. See that class for why.
It deliberately carries **no executable price levels** no entry price,
no stop-loss, no position size. This project is a research and education
implementation of the upstream TradingAgents framework, and concrete trade
levels for a named security are what turn a research tool into an
investment-advisory product. The capability is not shipped here; a
downstream fork that wants it can add it under its own responsibility.
"""
action: TraderAction = Field(
@ -130,37 +132,6 @@ class TraderProposal(BaseModel):
)
class TraderProposalWithLevels(TraderProposal):
"""Opt-in variant that additionally asks for executable price levels.
Only used when ``enable_execution_levels`` is True in the config. It is
off by default: this project is a research/education implementation of
the upstream TradingAgents framework, and concrete entry / stop-loss /
position-size levels for a named security are exactly the kind of output
that turns a research tool into an investment-advisory product. Operators
who turn this on are responsible for how the output is used and for any
licensing their jurisdiction requires.
"""
entry_price: Optional[float] = Field(
default=None,
description="Optional entry price target in the instrument's quote currency.",
)
stop_loss: Optional[float] = Field(
default=None,
description="Optional stop-loss price in the instrument's quote currency.",
)
position_sizing: Optional[str] = Field(
default=None,
description="Optional sizing guidance, e.g. '5% of portfolio'.",
)
def trader_proposal_model(enable_execution_levels: bool = False) -> type[TraderProposal]:
"""Pick the Trader schema matching the ``enable_execution_levels`` flag."""
return TraderProposalWithLevels if enable_execution_levels else TraderProposal
def render_trader_proposal(proposal: TraderProposal) -> str:
"""Render a TraderProposal to markdown.
@ -168,26 +139,13 @@ def render_trader_proposal(proposal: TraderProposal) -> str:
preserved for backward compatibility with the analyst stop-signal text
and any external code that greps for it.
"""
parts = [
return "\n".join([
f"**Action**: {proposal.action.value}",
"",
f"**Reasoning**: {proposal.reasoning}",
]
# getattr: the default TraderProposal has no level fields at all.
entry_price = getattr(proposal, "entry_price", None)
stop_loss = getattr(proposal, "stop_loss", None)
position_sizing = getattr(proposal, "position_sizing", None)
if entry_price is not None:
parts.extend(["", f"**Entry Price**: {entry_price}"])
if stop_loss is not None:
parts.extend(["", f"**Stop Loss**: {stop_loss}"])
if position_sizing:
parts.extend(["", f"**Position Sizing**: {position_sizing}"])
parts.extend([
"",
f"FINAL TRANSACTION PROPOSAL: **{proposal.action.value.upper()}**",
])
return "\n".join(parts)
# ---------------------------------------------------------------------------
@ -202,6 +160,9 @@ class PortfolioDecision(BaseModel):
extraction pass is required. Field descriptions double as the model's
output instructions, so the prompt body only needs to convey context and
the rating-scale guidance.
Like :class:`TraderProposal`, this carries no price target and no other
executable level see that class for why.
"""
rating: PortfolioRating = Field(
@ -230,33 +191,6 @@ class PortfolioDecision(BaseModel):
)
class PortfolioDecisionWithTarget(PortfolioDecision):
"""Opt-in variant that additionally asks for a price target.
Mirrors :class:`TraderProposalWithLevels` gated behind the same
``enable_execution_levels`` config flag, off by default. See that class
for the rationale.
"""
executive_summary: str = Field(
description=(
"A concise action plan covering entry strategy, position sizing, "
"key risk levels, and time horizon. Two to four sentences."
),
)
price_target: Optional[float] = Field(
default=None,
description="Optional target price in the instrument's quote currency.",
)
def portfolio_decision_model(
enable_execution_levels: bool = False,
) -> type[PortfolioDecision]:
"""Pick the Portfolio Manager schema matching the ``enable_execution_levels`` flag."""
return PortfolioDecisionWithTarget if enable_execution_levels else PortfolioDecision
def render_pm_decision(decision: PortfolioDecision) -> str:
"""Render a PortfolioDecision back to the markdown shape the rest of the system expects.
@ -272,10 +206,6 @@ def render_pm_decision(decision: PortfolioDecision) -> str:
"",
f"**Investment Thesis**: {decision.investment_thesis}",
]
# getattr: the default PortfolioDecision has no price_target field at all.
price_target = getattr(decision, "price_target", None)
if price_target is not None:
parts.extend(["", f"**Price Target**: {price_target}"])
if decision.time_horizon:
parts.extend(["", f"**Time Horizon**: {decision.time_horizon}"])
return "\n".join(parts)

View File

@ -6,30 +6,23 @@ import functools
from langchain_core.messages import AIMessage
from tradingagents.agents.schemas import render_trader_proposal, trader_proposal_model
from tradingagents.agents.schemas import TraderProposal, render_trader_proposal
from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction
from tradingagents.agents.utils.structured import (
bind_structured,
invoke_structured_or_freetext,
)
# Instruction appended when execution levels are off (the default). Keeps the
# model from re-introducing entry/stop/size levels in the free-text reasoning,
# which the schema alone cannot prevent.
# The schema alone cannot stop the model from putting price levels into the
# free-text reasoning field, so the prompt says it explicitly too.
_NO_LEVELS_INSTRUCTION = (
"Explain the reasoning behind the direction. Do NOT state entry prices, "
"stop-loss levels, target prices or position sizes for this security."
)
_LEVELS_INSTRUCTION = "Be specific about entry price, stop loss, and position sizing."
def create_trader(llm, enable_execution_levels: bool = False):
structured_llm = bind_structured(
llm, trader_proposal_model(enable_execution_levels), "Trader"
)
levels_instruction = (
_LEVELS_INSTRUCTION if enable_execution_levels else _NO_LEVELS_INSTRUCTION
)
def create_trader(llm):
structured_llm = bind_structured(llm, TraderProposal, "Trader")
def trader_node(state, name):
company_name = state["company_of_interest"]
@ -63,7 +56,7 @@ def create_trader(llm, enable_execution_levels: bool = False):
"- Minimum lot: 100 shares (main board) or 200 shares (STAR/ChiNext)\n"
"- Trading hours: 09:30-11:30, 13:00-15:00 Beijing time\n"
"Anchor your reasoning in the analysts' reports and the research plan. "
f"{levels_instruction} "
f"{_NO_LEVELS_INSTRUCTION} "
"(以上参数仅供技术研究参考,不构成投资建议)"
),
},

View File

@ -38,13 +38,6 @@ DEFAULT_CONFIG = {
# start date (default: first day of the current month → "monthly" view);
# None keeps the previous behaviour (the model's own default, ~30). (#16)
"market_lookback_days": None,
# Executable price levels (entry / stop-loss / position size / price target).
# OFF by default: this project is a research and education implementation of
# the upstream TradingAgents framework, and concrete trade levels for a named
# security are what turn a research tool into an investment-advisory product.
# Turning this on is the operator's decision, and the operator is responsible
# for how the output is used and for any licensing their jurisdiction requires.
"enable_execution_levels": False,
# Debate and discussion settings
"max_debate_rounds": 1,
"max_risk_discuss_rounds": 1,

View File

@ -19,17 +19,12 @@ class GraphSetup:
deep_thinking_llm: Any,
tool_nodes: Dict[str, ToolNode],
conditional_logic: ConditionalLogic,
enable_execution_levels: bool = False,
):
"""Initialize with required components.
``enable_execution_levels`` is off by default; see ``default_config.py``.
"""
"""Initialize with required components."""
self.quick_thinking_llm = quick_thinking_llm
self.deep_thinking_llm = deep_thinking_llm
self.tool_nodes = tool_nodes
self.conditional_logic = conditional_logic
self.enable_execution_levels = enable_execution_levels
def setup_graph(
self, selected_analysts=["market", "social", "news", "fundamentals", "policy", "hot_money", "lockup"]
@ -110,15 +105,13 @@ class GraphSetup:
bull_researcher_node = create_bull_researcher(self.quick_thinking_llm)
bear_researcher_node = create_bear_researcher(self.quick_thinking_llm)
research_manager_node = create_research_manager(self.deep_thinking_llm)
trader_node = create_trader(self.quick_thinking_llm, self.enable_execution_levels)
trader_node = create_trader(self.quick_thinking_llm)
# Create risk analysis nodes
aggressive_analyst = create_aggressive_debator(self.quick_thinking_llm)
neutral_analyst = create_neutral_debator(self.quick_thinking_llm)
conservative_analyst = create_conservative_debator(self.quick_thinking_llm)
portfolio_manager_node = create_portfolio_manager(
self.deep_thinking_llm, self.enable_execution_levels
)
portfolio_manager_node = create_portfolio_manager(self.deep_thinking_llm)
# Create workflow
workflow = StateGraph(AgentState)

View File

@ -122,7 +122,6 @@ class TradingAgentsGraph:
self.deep_thinking_llm,
self.tool_nodes,
self.conditional_logic,
self.config.get("enable_execution_levels", False),
)
self.propagator = Propagator()