feat: integration finalised

This commit is contained in:
Slava Nikitin 2025-12-01 11:52:22 +09:00
parent 2906e5a570
commit 260527ed9b
8 changed files with 471 additions and 28 deletions

View File

@ -127,6 +127,11 @@ cp .env.example .env
# Edit .env with your actual API keys
```
Run a quick preflight to verify env vars and writable result directories:
```bash
python scripts/preflight_check.py
```
**Note:** We are happy to partner with Alpha Vantage to provide robust API support for TradingAgents. You can get a free AlphaVantage API [here](https://www.alphavantage.co/support/#api-key), TradingAgents-sourced requests also have increased rate limits to 60 requests per minute with no daily limits. Typically the quota is sufficient for performing complex tasks with TradingAgents thanks to Alpha Vantages open-source support program. If you prefer to use OpenAI for these data sources instead, you can modify the data vendor settings in `tradingagents/default_config.py`.
### CLI Usage
@ -256,6 +261,14 @@ config["portfolio_orchestrator"]["trade_activation"]["priority_threshold"] = 0.8
You can view the full list of configurations in `tradingagents/default_config.py`.
### Development & tests
Install dev extras to run the test suite:
```bash
pip install -r requirements-dev.txt
pytest
```
## Contributing
We welcome contributions from the community! Whether it's fixing a bug, improving documentation, or suggesting a new feature, your input helps make this project better. If you are interested in this line of research, please consider joining our open-source financial AI research community [Tauric Research](https://tauric.ai/).

50
main.py
View File

@ -316,6 +316,19 @@ def _process_autopilot_events(worker: AutopilotWorker) -> None:
console.print(table)
def _resolve_premarket_window(value: Any) -> int:
candidates = [os.getenv("AUTOPILOT_PREMARKET_MINUTES"), value, 30]
for candidate in candidates:
if candidate in (None, ""):
continue
try:
minutes = int(candidate)
except (TypeError, ValueError):
continue
return max(minutes, 0)
return 0
def _run_price_alert_poll(broker: AutopilotBroker) -> None:
outcomes = broker.poll_once()
if not outcomes:
@ -528,7 +541,7 @@ def _run_autopilot_loop(
event_interval = max(int(autopilot_cfg.get("event_loop_interval_seconds", 10)), 1)
price_poll_interval = max(int(autopilot_cfg.get("price_poll_interval_seconds", 60)), event_interval)
seed_run = bool(autopilot_cfg.get("auto_trade_on_start", True))
premarket_window = max(int(autopilot_cfg.get("pre_market_research_minutes", 30)), 0)
premarket_window = _resolve_premarket_window(autopilot_cfg.get("pre_market_research_minutes"))
console.print("Autopilot mode enabled. Press Ctrl+C to stop.", style="bold cyan")
@ -546,13 +559,23 @@ def _run_autopilot_loop(
console.print(f"Failed to refresh account snapshot: {exc}", style="red")
return None
def _refresh_snapshot_if_needed(force: bool = False) -> Optional[AccountSnapshot]:
nonlocal latest_snapshot, market_is_open, market_status
if not force and not market_is_open and not _within_premarket(market_status, premarket_window):
return latest_snapshot
snap = _refresh_snapshot()
return snap
heartbeat_interval = max(event_interval, 30)
market_check_interval = max(price_poll_interval, 60)
market_status = _get_market_status(auto_trader)
last_market_check = time.time()
market_is_open = bool(market_status.get("is_open"))
next_market_check_delay = market_check_interval
if seed_run:
if market_is_open:
snap = _refresh_snapshot()
snap = _refresh_snapshot_if_needed(force=True)
if snap:
ran = _execute_auto_trade(
auto_trader,
@ -568,7 +591,7 @@ def _run_autopilot_loop(
reason = market_status.get("clock_text") or market_status.get("reason") or "market closed"
console.print(f"Initial run skipped: {reason}.", style="yellow")
if premarket_window > 0 and _should_run_premarket(market_status, premarket_window):
snap = _refresh_snapshot()
snap = _refresh_snapshot_if_needed(force=True)
if snap and _execute_auto_trade(
auto_trader,
snap,
@ -591,8 +614,6 @@ def _run_autopilot_loop(
last_price_poll = 0.0
last_signature = ""
last_heartbeat = 0.0
heartbeat_interval = max(event_interval, 30)
market_check_interval = max(price_poll_interval, 60)
events_since_heartbeat = 0
console.print(
f"Entering autopilot loop (event every {event_interval}s, price poll every {price_poll_interval}s)…",
@ -600,8 +621,6 @@ def _run_autopilot_loop(
)
try:
market_status = _get_market_status(auto_trader)
last_market_check = time.time()
while True:
events_since_heartbeat += _drain_autopilot_queue(autopilot_worker)
@ -626,13 +645,15 @@ def _run_autopilot_loop(
events_since_heartbeat = 0
last_heartbeat = now
if now - last_market_check >= market_check_interval:
if now - last_market_check >= next_market_check_delay:
market_status = _get_market_status(auto_trader)
last_market_check = now
is_open = bool(market_status.get("is_open"))
next_market_check_delay = market_check_interval if is_open else max(market_check_interval * 5, 300)
market_is_open = is_open
if is_open:
if pending_market_open_run:
snap = _refresh_snapshot()
snap = _refresh_snapshot_if_needed(force=True)
if snap and _execute_auto_trade(
auto_trader,
snap,
@ -841,12 +862,21 @@ def _should_run_premarket(status: Dict[str, Any], window_minutes: int) -> bool:
next_open = _parse_market_time(status.get("next_open"))
if not next_open:
return False
now_utc = datetime.now(timezone.utc)
current = _parse_market_time(status.get("current_time"))
if current is None:
current = datetime.now(timezone.utc)
now_utc = current.astimezone(timezone.utc)
target = next_open.astimezone(timezone.utc)
minutes = (target - now_utc).total_seconds() / 60
return 0 <= minutes <= window_minutes
def _within_premarket(status: Dict[str, Any], window_minutes: int) -> bool:
if not window_minutes:
return False
return _should_run_premarket(status, window_minutes)
def _parse_market_time(value: Optional[str]) -> Optional[datetime]:
if not value:
return None

1
requirements-dev.txt Normal file
View File

@ -0,0 +1 @@
pytest>=8.3.0

111
scripts/preflight_check.py Normal file
View File

@ -0,0 +1,111 @@
from __future__ import annotations
import os
import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tradingagents.default_config import DEFAULT_CONFIG
def _mask(value: str) -> str:
if not value:
return "<missing>"
if len(value) <= 4:
return "***"
return f"{value[:2]}***{value[-2:]}"
def _check_env(var: str) -> Tuple[bool, str]:
value = os.getenv(var, "")
return (bool(value), _mask(value))
def _check_writable(path: Path) -> Tuple[bool, str]:
try:
path.mkdir(parents=True, exist_ok=True)
probe = path / ".preflight_probe"
probe.write_text("ok", encoding="utf-8")
probe.unlink(missing_ok=True)
return True, "writable"
except Exception as exc:
return False, f"not writable: {exc}"
def _print_block(title: str, rows: List[Tuple[str, Any]]) -> None:
print(f"\n{title}")
print("-" * len(title))
for key, value in rows:
print(f"{key}: {value}")
def run() -> int:
config: Dict[str, Any] = DEFAULT_CONFIG.copy()
results_dir = Path(config.get("results_dir", "./results"))
memory_cfg = (config.get("auto_trade", {}) or {}).get("memory", {}) or {}
memory_dir = Path(memory_cfg.get("dir", results_dir / "memory"))
env_rows: List[Tuple[str, Any]] = []
for var in ("OPENAI_API_KEY", "APCA_API_KEY_ID", "APCA_API_SECRET_KEY"):
present, masked = _check_env(var)
env_rows.append((var, masked if present else "<missing>"))
alpaca_cfg = config.get("alpaca_mcp", {}) or {}
alpaca_rows = [
("enabled", alpaca_cfg.get("enabled", False)),
("base_url", alpaca_cfg.get("base_url") or "<unset>"),
("host", alpaca_cfg.get("host") or "<unset>"),
("port", alpaca_cfg.get("port") or "<unset>"),
("transport", alpaca_cfg.get("transport") or "<unset>"),
("required_tools", ", ".join(alpaca_cfg.get("required_tools", []))),
]
if alpaca_cfg.get("enabled"):
missing_tool = not alpaca_cfg.get("required_tools")
if missing_tool:
alpaca_rows.append(("warning", "alpaca_mcp.enabled=true but required_tools is empty"))
fs_rows = []
writable_results, results_msg = _check_writable(results_dir)
writable_memory, memory_msg = _check_writable(memory_dir)
fs_rows.append(("results_dir", f"{results_dir} ({results_msg})"))
fs_rows.append(("memory_dir", f"{memory_dir} ({memory_msg})"))
trade_exec = config.get("trade_execution", {}) or {}
exec_rows = [
("enabled", trade_exec.get("enabled", False)),
("dry_run", trade_exec.get("dry_run", True)),
("default_order_quantity", trade_exec.get("default_order_quantity")),
("time_in_force", trade_exec.get("time_in_force")),
]
print("TradingAgents preflight")
print("=======================")
_print_block("Environment", env_rows)
_print_block("Alpaca MCP config", alpaca_rows)
_print_block("Filesystem", fs_rows)
_print_block("Trade execution", exec_rows)
warnings: List[str] = []
if not _check_env("OPENAI_API_KEY")[0]:
warnings.append("OPENAI_API_KEY is missing")
if alpaca_cfg.get("enabled") and (not _check_env("APCA_API_KEY_ID")[0] or not _check_env("APCA_API_SECRET_KEY")[0]):
warnings.append("Alpaca keys missing while alpaca_mcp.enabled=true")
if not writable_results or not writable_memory:
warnings.append("results_dir or memory_dir is not writable")
if warnings:
print("\nWarnings:")
for item in warnings:
print(f"- {item}")
return 1
print("\nPreflight checks passed.")
return 0
if __name__ == "__main__":
raise SystemExit(run())

12
tests/test_smoke.py Normal file
View File

@ -0,0 +1,12 @@
def test_default_config_present():
from tradingagents import default_config
cfg = default_config.DEFAULT_CONFIG
assert isinstance(cfg, dict)
assert "results_dir" in cfg
def test_preflight_imports():
import scripts.preflight_check as preflight
assert callable(preflight.run)

View File

@ -3,7 +3,13 @@ import os
DEFAULT_CONFIG = {
"project_dir": os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
"results_dir": os.getenv("TRADINGAGENTS_RESULTS_DIR", "./results"),
"data_dir": "/Users/yluo/Documents/Code/ScAI/FR1-data",
"data_dir": os.getenv(
"TRADINGAGENTS_DATA_DIR",
os.path.join(
os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
"data",
),
),
"data_cache_dir": os.path.join(
os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
"dataflows/data_cache",

View File

@ -396,11 +396,39 @@ class TradingAgentsGraph:
normalized = clock_text.lower()
is_open = "is open: yes" in normalized
parsed = self._parse_market_clock(clock_text)
return {
"is_open": is_open,
"clock_text": clock_text,
**parsed,
}
def _parse_market_clock(self, clock_text: str) -> Dict[str, str]:
current_time = None
next_open = None
next_close = None
for line in clock_text.splitlines():
line = line.strip()
if not line or ":" not in line:
continue
label, value = line.split(":", 1)
label = label.strip().lower()
value = value.strip()
if label == "current time":
current_time = value
elif label == "next open":
next_open = value
elif label == "next close":
next_close = value
payload: Dict[str, str] = {}
if current_time:
payload["current_time"] = current_time
if next_open:
payload["next_open"] = next_open
if next_close:
payload["next_close"] = next_close
return payload
def _generate_plan_with_llm(self, payload: Dict[str, Any]) -> Dict[str, Any]:
system_prompt = (
"You are the sequential planning engine for TradingAgents. "
@ -459,7 +487,15 @@ class TradingAgentsGraph:
}
def _maybe_execute_trade(self, final_state: Dict[str, Any], decision_text: str) -> Dict[str, Any]:
def _maybe_execute_trade(
self,
final_state: Dict[str, Any],
decision_text: str,
*,
quantity: Optional[float] = None,
notional: Optional[float] = None,
reference_price: Optional[float] = None,
) -> Dict[str, Any]:
exec_cfg = self.trade_execution_config or {}
if not exec_cfg.get("enabled"):
return {"status": "disabled", "reason": "trade_execution_disabled"}
@ -472,8 +508,59 @@ class TradingAgentsGraph:
if action not in {"BUY", "SELL"}:
return {"status": "skipped", "reason": f"action_{action}"}
quantity = float(exec_cfg.get("default_order_quantity", 0))
if quantity <= 0:
def _as_quantity(value: Any) -> Optional[float]:
try:
qty = float(value)
except (TypeError, ValueError):
return None
return qty if qty > 0 else None
resolved_qty = _as_quantity(quantity)
ref_price_value = _as_quantity(reference_price)
if not ref_price_value and isinstance(final_state, dict):
ref_price_value = _as_quantity(final_state.get("reference_price"))
if resolved_qty is None and notional not in (None, ""):
try:
notional_value = float(notional)
except (TypeError, ValueError):
notional_value = None
if notional_value and ref_price_value:
computed = int(notional_value // ref_price_value)
resolved_qty = float(computed) if computed > 0 else None
if resolved_qty is None:
resolved_qty = _as_quantity(exec_cfg.get("default_order_quantity", 0))
if not ref_price_value and resolved_qty and ref_price_value is None:
# Best effort: try to recover reference price from hypothesis/trader notes if present
ref_price_value = _as_quantity(final_state.get("reference_price")) if isinstance(final_state, dict) else None
# Guard against exceeding buying power if price is available
if resolved_qty and ref_price_value:
try:
client = self._get_alpaca_client()
if client:
account_text = client.fetch_account_info()
buying_power = self._parse_buying_power(account_text)
estimated_cost = resolved_qty * ref_price_value
if buying_power and estimated_cost > buying_power:
capped = int(buying_power // ref_price_value)
if capped <= 0:
return {
"status": "skipped",
"reason": "insufficient_buying_power",
"buying_power": buying_power,
"requested_qty": resolved_qty,
"reference_price": ref_price_value,
}
resolved_qty = float(capped)
except Exception:
# Fail open on guard; order placement will still respect dry_run flag
pass
if resolved_qty is None or resolved_qty <= 0:
return {"status": "skipped", "reason": "invalid_quantity"}
client = self._get_alpaca_client()
@ -485,7 +572,7 @@ class TradingAgentsGraph:
"side": "buy" if action == "BUY" else "sell",
"order_type": "market",
"time_in_force": exec_cfg.get("time_in_force", "day").upper(),
"quantity": float(quantity),
"quantity": float(resolved_qty),
}
try:
@ -516,6 +603,20 @@ class TradingAgentsGraph:
self.logger.exception("Unexpected error during order submission")
return {"status": "failed", "reason": str(exc), "payload": payload}
def _parse_buying_power(self, account_text: str) -> float:
for line in account_text.splitlines():
if "buying power" not in line.lower():
continue
parts = line.split(":", 1)
if len(parts) != 2:
continue
value = parts[1].strip().replace("$", "").replace(",", "")
try:
return float(value)
except ValueError:
continue
return 0.0
def _extract_action(self, decision_text: str) -> str:
if not decision_text:
return "UNKNOWN"
@ -634,11 +735,25 @@ class TradingAgentsGraph:
self._write_run_summary(final_state, processed_result)
return final_state, processed_result
def execute_trade_directive(self, symbol: str, action: str) -> Dict[str, Any]:
def execute_trade_directive(
self,
symbol: str,
action: str,
*,
quantity: Optional[float] = None,
notional: Optional[float] = None,
reference_price: Optional[float] = None,
) -> Dict[str, Any]:
"""Execute a trade directive issued outside the standard graph run."""
directive = (action or "").strip().upper()
minimal_state = {"company_of_interest": symbol}
return self._maybe_execute_trade(minimal_state, directive)
return self._maybe_execute_trade(
minimal_state,
directive,
quantity=quantity,
notional=notional,
reference_price=reference_price,
)
def _log_state(self, trade_date, final_state):
"""Log the final state to a JSON file."""

View File

@ -99,6 +99,9 @@ class TradingToolbox:
self.logger = logger or logging.getLogger(__name__)
self.memory_store = memory_store
self._agent_runners = self._init_agent_runners()
self._trade_tool_enabled = bool(
(self.config.get("auto_trade", {}) or {}).get("responses_enable_trade_tool")
)
self._tools = self._build_tools()
@property
@ -194,22 +197,38 @@ class TradingToolbox:
},
handler=self._tool_fetch_indicators,
),
"submit_trade_order": ResponsesTool(
}
if self._trade_tool_enabled:
tools["submit_trade_order"] = ResponsesTool(
name="submit_trade_order",
description="Submit a trade directive (BUY/SELL/HOLD) for a ticker with optional reasoning. Honors dry-run and market-open checks.",
description=(
"Submit a trade directive (BUY/SELL/HOLD) for a ticker with optional sizing. Honors dry-run and market-open checks. "
"Provide quantity in shares when you can; otherwise pass notional and a reference price to convert."
),
schema={
"type": "object",
"properties": {
"symbol": {"type": "string"},
"action": {"type": "string", "enum": ["BUY", "SELL", "HOLD"]},
"quantity": {
"type": "number",
"description": "Number of shares to trade (preferred).",
},
"notional": {
"type": "number",
"description": "Dollar amount to allocate; requires reference_price for conversion.",
},
"reference_price": {
"type": "number",
"description": "Price used to convert notional to shares; use your latest fetch_market_data/fetch_indicators price.",
},
"notes": {"type": "string"},
},
"required": ["symbol", "action"],
"additionalProperties": False,
},
handler=self._tool_submit_trade,
),
}
)
if self.memory_store and self.memory_store.is_enabled():
tools["get_ticker_memory"] = ResponsesTool(
name="get_ticker_memory",
@ -306,6 +325,9 @@ class TradingToolbox:
def _tool_submit_trade(self, args: Dict[str, Any]) -> Dict[str, Any]:
symbol = str(args.get("symbol") or "").upper()
action = str(args.get("action") or "").upper()
quantity = args.get("quantity")
notional = args.get("notional")
reference_price = args.get("reference_price")
status = self.graph.check_market_status()
market_open = bool(status.get("is_open", True))
if not market_open:
@ -313,7 +335,13 @@ class TradingToolbox:
"status": "market_closed",
"clock": status.get("clock_text"),
}
result = self.graph.execute_trade_directive(symbol, action)
result = self.graph.execute_trade_directive(
symbol,
action,
quantity=quantity,
notional=notional,
reference_price=reference_price,
)
return {"status": result.get("status"), "response": result}
def _call_vendor(self, method: str, *args) -> Any:
@ -419,6 +447,19 @@ class ResponsesAutoTradeService:
max_entries = int(memory_cfg.get("max_entries", 5))
self.memory_store = TickerMemoryStore(memory_dir, max_entries=max_entries, enabled=memory_enabled)
self._strategy_brief_cache = self._strategy_presets_brief()
auto_trade_cfg = self.config.get("auto_trade", {}) or {}
self.trade_tool_enabled = bool(auto_trade_cfg.get("responses_enable_trade_tool"))
self.plan_followup_limit = max(int(auto_trade_cfg.get("responses_plan_followup_limit", 2)), 0)
self._plan_status_done_values = {
"done",
"complete",
"completed",
"skipped",
"skip",
"n/a",
"na",
"not_applicable",
}
def run(self, snapshot: AccountSnapshot, *, focus_override: Optional[List[str]] = None) -> AutoTradeResult:
self._reference_prices = _snapshot_reference_prices(snapshot)
@ -527,6 +568,8 @@ class ResponsesAutoTradeService:
submitted_trades=submitted_trades,
)
final_text = self._response_text(response)
if final_text:
conversation.append({"role": "assistant", "content": final_text})
summary = _extract_json_block(final_text)
if not summary.get("decisions"):
@ -554,14 +597,58 @@ class ResponsesAutoTradeService:
submitted_trades=submitted_trades,
)
final_text = self._response_text(response)
if final_text:
conversation.append({"role": "assistant", "content": final_text})
summary = _extract_json_block(final_text)
guard_info = self._plan_guard(summary)
followups = 0
while (
summary.get("decisions")
and guard_info.get("needs_followup")
and followups < self.plan_followup_limit
):
followups += 1
conversation.append(
{
"role": "user",
"content": json.dumps(
{
"plan_validation": {
"status": "incomplete",
"details": guard_info.get("details"),
"instruction": (
"Finish or explicitly skip (with justification) every plan step before "
"finalizing the decision summary. Continue executing the scheduled tools; do "
"not place trades until no steps remain pending."
),
}
}
),
}
)
response = self._responses_call(
conversation,
toolbox,
transcript,
allow_tools=True,
submitted_trades=submitted_trades,
)
final_text = self._response_text(response)
if final_text:
conversation.append({"role": "assistant", "content": final_text})
summary = _extract_json_block(final_text)
guard_info = self._plan_guard(summary)
decisions, focus = self._decisions_from_summary(summary)
raw_state = {
"responses_transcript": transcript,
"responses_summary": summary,
"responses_output_text": final_text,
"plan_guard": guard_info,
}
if guard_info.get("needs_followup") and guard_info.get("reason"):
raw_state.setdefault("skip_reason", guard_info.get("reason"))
if self.memory_store and self.memory_store.is_enabled() and decisions:
payload: List[Dict[str, Any]] = []
for decision in decisions:
@ -571,7 +658,12 @@ class ResponsesAutoTradeService:
decision_dict["priority"] = decision.priority
payload.append(decision_dict)
self.memory_store.record_decisions(payload)
self._auto_execute_trades(decisions, submitted_trades)
self._auto_execute_trades(
decisions,
submitted_trades,
allow_execution=not bool(guard_info.get("blocked_actions")),
guard_reason=guard_info.get("reason"),
)
return AutoTradeResult(
focus_tickers=focus or focus_tickers,
@ -781,6 +873,51 @@ class ResponsesAutoTradeService:
derived.append(f"price <= {failure_price:.2f}")
return base_triggers + derived
def _plan_guard(self, summary: Dict[str, Any]) -> Dict[str, Any]:
decisions = summary.get("decisions") or []
details: List[Dict[str, Any]] = []
blocked: List[str] = []
for entry in decisions:
ticker = str(entry.get("ticker") or "").upper()
if not ticker:
continue
statuses = entry.get("plan_status") or {}
incomplete: List[str] = []
if isinstance(statuses, dict) and statuses:
for step, status in statuses.items():
label = str(step or "").strip() or "<unnamed step>"
status_text = str(status or "").strip()
normalized = status_text.lower()
ok = (
normalized in self._plan_status_done_values
or "done" in normalized
or "complete" in normalized
or "skip" in normalized
or "n/a" in normalized
)
if ok:
continue
display = f"{label} ({status_text or 'pending'})"
incomplete.append(display)
elif entry.get("plan_actions"):
for action in entry.get("plan_actions") or []:
incomplete.append(f"{action} (no status reported)")
if incomplete:
details.append({"ticker": ticker, "steps": incomplete})
action = str(entry.get("action") or "").upper()
if action in {"BUY", "SELL"} and ticker not in blocked:
blocked.append(ticker)
reason = ""
if details:
joined = "; ".join(f"{item['ticker']}: {', '.join(item['steps'])}" for item in details)
reason = f"Plan validation incomplete; pending steps -> {joined}"
return {
"needs_followup": bool(details),
"blocked_actions": blocked,
"details": details,
"reason": reason,
}
def _extract_tool_calls(self, response: Any) -> List[Dict[str, Any]]:
calls: List[Dict[str, Any]] = []
output_items = getattr(response, "output", []) or []
@ -857,10 +994,20 @@ class ResponsesAutoTradeService:
self,
decisions: List[TickerDecision],
submitted_trades: Set[Tuple[str, str]],
*,
allow_execution: bool = True,
guard_reason: Optional[str] = None,
) -> None:
exec_cfg = self.config.get("trade_execution", {}) or {}
if not exec_cfg.get("enabled"):
return
if not allow_execution:
message = guard_reason or "Plan guard blocked trade execution."
try:
print(f"[Auto Execution] Skipping trade execution: {message}")
except Exception:
pass
return
for decision in decisions:
action = (decision.final_decision or decision.immediate_action or "").upper()
if action not in {"BUY", "SELL"}:
@ -875,6 +1022,13 @@ class ResponsesAutoTradeService:
pass
def _build_system_prompt(self) -> str:
trade_clause = (
"After producing the JSON, call `submit_trade_order` for every ticker whose action is BUY or SELL "
"(subject to trade execution settings)."
if self.trade_tool_enabled
else "Do not call `submit_trade_order`; once your plan summary shows every step resolved, the autopilot "
"will handle trade submission automatically."
)
return (
"You are the trading orchestrator for TradingAgents. Every run must begin by calling "
"`get_account_overview` exactly once (unless you explicitly refresh the Alpaca snapshot) and narrating the "
@ -898,17 +1052,18 @@ class ResponsesAutoTradeService:
"conditions that complete or cancel the hypothesis so automation can act on them.\n\n"
"Narrate every step before you make the tool call so the CLI can display your thinking live, and summarize what "
"you learned from each tool. Be curious: when a tickers context is thin, proactively explore the smallest set "
"of tools needed to form a defendable hypothesis rather than defaulting to HOLD. Maintain an explicit plan tracker: list "+
"of tools needed to form a defendable hypothesis rather than defaulting to HOLD. Maintain an explicit plan tracker: list "
"each planned action (e.g., Step 1 Fetch TSLA market data) along with its status (`pending`, `in_progress`, `done`), "
"and after every tool call, announce which step changed status and why. Consider market-open status before "
"trading, respect trade execution limits (but treat `day_trades_remaining` as informational—you may still "
"recommend buys/sells), and keep narration concise but informative.\n\n"
"recommend buys/sells), and keep narration concise but informative. Before the final summary, resolve every "
"`plan_status` entry: either run the scheduled step or explicitly mark it as `skipped` with a short reason so "
"no action remains `pending`.\n\n"
"Conclude with a JSON summary containing decisions for each ticker. The final assistant message must include a "
"JSON object with a `decisions` array where each entry specifies `ticker`, `action`, `priority`, "
"`plan_actions`, `next_decision`, `notes`, `plan_status` (a mapping of each plan action to its status), the `strategy` object described above, "
"and optional `action_queue` and `execution_plan` fields. After "
"producing the JSON, call `submit_trade_order` for every ticker whose action is BUY or SELL (subject to trade "
"execution settings)."
"and optional `action_queue` and `execution_plan` fields. "
f"{trade_clause}"
)
def _safe_json(self, raw: str) -> Dict[str, Any]: