feat: integration finalised
This commit is contained in:
parent
2906e5a570
commit
260527ed9b
13
README.md
13
README.md
|
|
@ -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 Vantage’s 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
50
main.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
pytest>=8.3.0
|
||||
|
|
@ -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())
|
||||
|
|
@ -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)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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 ticker’s 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]:
|
||||
|
|
|
|||
Loading…
Reference in New Issue