Merge branch 'main' into feature/portfolio-resumability-and-cleanup
This commit is contained in:
commit
2a05e4b5a9
|
|
@ -149,6 +149,8 @@ async def get_latest_portfolio_state(
|
||||||
"price": d.get("price", 0.0),
|
"price": d.get("price", 0.0),
|
||||||
"executed_at": d.get("trade_date", ""),
|
"executed_at": d.get("trade_date", ""),
|
||||||
"rationale": d.get("rationale"),
|
"rationale": d.get("rationale"),
|
||||||
|
"stop_loss": d.get("stop_loss"),
|
||||||
|
"take_profit": d.get("take_profit"),
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ interface Trade {
|
||||||
price: number;
|
price: number;
|
||||||
executed_at?: string;
|
executed_at?: string;
|
||||||
rationale?: string;
|
rationale?: string;
|
||||||
|
stop_loss?: number | null;
|
||||||
|
take_profit?: number | null;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,15 +221,35 @@ export const PortfolioViewer: React.FC<PortfolioViewerProps> = ({ defaultPortfol
|
||||||
border="1px solid"
|
border="1px solid"
|
||||||
borderColor="whiteAlpha.100"
|
borderColor="whiteAlpha.100"
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
align="center"
|
align="flex-start"
|
||||||
>
|
>
|
||||||
<HStack spacing={3}>
|
<HStack spacing={3} align="flex-start">
|
||||||
<Badge colorScheme={t.action?.toUpperCase() === 'BUY' ? 'green' : t.action?.toUpperCase() === 'SELL' ? 'red' : 'gray'}>
|
<Badge colorScheme={t.action?.toUpperCase() === 'BUY' ? 'green' : t.action?.toUpperCase() === 'SELL' ? 'red' : 'gray'}>
|
||||||
{t.action?.toUpperCase()}
|
{t.action?.toUpperCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<VStack align="flex-start" spacing={0}>
|
||||||
|
<HStack spacing={2}>
|
||||||
<Code colorScheme="cyan" fontSize="sm">{t.ticker}</Code>
|
<Code colorScheme="cyan" fontSize="sm">{t.ticker}</Code>
|
||||||
<Text fontSize="sm">{t.quantity} @ ${(t.price ?? 0).toFixed(2)}</Text>
|
<Text fontSize="sm">{t.quantity} @ ${(t.price ?? 0).toFixed(2)}</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
{(t.stop_loss != null || t.take_profit != null) && (
|
||||||
|
<HStack spacing={3} mt={1}>
|
||||||
|
{t.stop_loss != null && (
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Text fontSize="2xs" color="red.400">SL:</Text>
|
||||||
|
<Text fontSize="2xs" color="red.300" fontWeight="semibold">${t.stop_loss.toFixed(2)}</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
{t.take_profit != null && (
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Text fontSize="2xs" color="green.400">TP:</Text>
|
||||||
|
<Text fontSize="2xs" color="green.300" fontWeight="semibold">${t.take_profit.toFixed(2)}</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
<VStack align="flex-end" spacing={0}>
|
<VStack align="flex-end" spacing={0}>
|
||||||
<Text fontSize="2xs" color="whiteAlpha.400">{t.executed_at || '—'}</Text>
|
<Text fontSize="2xs" color="whiteAlpha.400">{t.executed_at || '—'}</Text>
|
||||||
{t.rationale && (
|
{t.rationale && (
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# Current Milestone
|
# Current Milestone
|
||||||
|
|
||||||
AgentOS visual observability layer shipped. Portfolio Manager fully implemented (Phases 1–10). All 725 tests passing (14 skipped).
|
AgentOS visual observability layer shipped. Portfolio Manager fully implemented (Phases 1–10). All 725 tests passing (14 skipped). Stop-loss / take-profit fields added to trades.
|
||||||
|
|
||||||
# Recent Progress
|
# Recent Progress
|
||||||
|
|
||||||
|
- **Stop-loss & Take-profit on trades**: Added `stop_loss` and `take_profit` optional fields to the `Trade` model, SQL migration, PM agent prompt, trade executor, repository, API route, and frontend Trade History tab.
|
||||||
- **AgentOS (current PR)**: Full-stack visual observability layer for agent execution
|
- **AgentOS (current PR)**: Full-stack visual observability layer for agent execution
|
||||||
- `agent_os/backend/` — FastAPI backend (port 8088) with REST + WebSocket streaming
|
- `agent_os/backend/` — FastAPI backend (port 8088) with REST + WebSocket streaming
|
||||||
- `agent_os/frontend/` — React + Vite 8 + Chakra UI + ReactFlow dashboard
|
- `agent_os/frontend/` — React + Vite 8 + Chakra UI + ReactFlow dashboard
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,8 @@ Immutable record of a single mock trade execution. Never modified after creation
|
||||||
| `trade_date` | `str` | Yes | ISO-8601 UTC datetime of execution |
|
| `trade_date` | `str` | Yes | ISO-8601 UTC datetime of execution |
|
||||||
| `rationale` | `str \| None` | No | PM agent rationale for this trade |
|
| `rationale` | `str \| None` | No | PM agent rationale for this trade |
|
||||||
| `signal_source` | `str \| None` | No | `"scanner"`, `"holding_review"`, `"pm_agent"` |
|
| `signal_source` | `str \| None` | No | `"scanner"`, `"holding_review"`, `"pm_agent"` |
|
||||||
|
| `stop_loss` | `float \| None` | No | Price level at which the position should be exited to limit loss |
|
||||||
|
| `take_profit` | `float \| None` | No | Price target at which the position should be sold for profit |
|
||||||
| `metadata` | `dict` | No | Free-form JSON |
|
| `metadata` | `dict` | No | Free-form JSON |
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,8 @@ CREATE TABLE IF NOT EXISTS trades (
|
||||||
trade_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
trade_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
rationale TEXT,
|
rationale TEXT,
|
||||||
signal_source TEXT,
|
signal_source TEXT,
|
||||||
|
stop_loss NUMERIC(18,4) CHECK (stop_loss IS NULL OR stop_loss > 0),
|
||||||
|
take_profit NUMERIC(18,4) CHECK (take_profit IS NULL OR take_profit > 0),
|
||||||
metadata JSONB NOT NULL DEFAULT '{}',
|
metadata JSONB NOT NULL DEFAULT '{}',
|
||||||
|
|
||||||
CONSTRAINT trades_action_values CHECK (action IN ('BUY', 'SELL'))
|
CONSTRAINT trades_action_values CHECK (action IN ('BUY', 'SELL'))
|
||||||
|
|
@ -85,6 +87,8 @@ CREATE TABLE IF NOT EXISTS trades (
|
||||||
**Constraints:**
|
**Constraints:**
|
||||||
- `action IN ('BUY', 'SELL')` — only two valid actions
|
- `action IN ('BUY', 'SELL')` — only two valid actions
|
||||||
- `shares > 0`, `price > 0` — all quantities positive
|
- `shares > 0`, `price > 0` — all quantities positive
|
||||||
|
- `stop_loss > 0` (when set) — stop-loss price must be positive; NULL means not specified
|
||||||
|
- `take_profit > 0` (when set) — take-profit target must be positive; NULL means not specified
|
||||||
- No `updated_at` — trades are immutable
|
- No `updated_at` — trades are immutable
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -113,9 +113,58 @@ def test_trade_to_dict_round_trip(sample_trade):
|
||||||
assert restored.trade_date == sample_trade.trade_date
|
assert restored.trade_date == sample_trade.trade_date
|
||||||
assert restored.rationale == sample_trade.rationale
|
assert restored.rationale == sample_trade.rationale
|
||||||
assert restored.signal_source == sample_trade.signal_source
|
assert restored.signal_source == sample_trade.signal_source
|
||||||
|
assert restored.stop_loss == sample_trade.stop_loss
|
||||||
|
assert restored.take_profit == sample_trade.take_profit
|
||||||
assert restored.metadata == sample_trade.metadata
|
assert restored.metadata == sample_trade.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_stop_loss_take_profit_round_trip(sample_portfolio_id):
|
||||||
|
"""Trade with stop_loss and take_profit serialises and deserialises correctly."""
|
||||||
|
trade = Trade(
|
||||||
|
trade_id="t-risk-1",
|
||||||
|
portfolio_id=sample_portfolio_id,
|
||||||
|
ticker="NVDA",
|
||||||
|
action="BUY",
|
||||||
|
shares=10.0,
|
||||||
|
price=800.0,
|
||||||
|
total_value=8_000.0,
|
||||||
|
stop_loss=720.0,
|
||||||
|
take_profit=960.0,
|
||||||
|
)
|
||||||
|
d = trade.to_dict()
|
||||||
|
assert d["stop_loss"] == 720.0
|
||||||
|
assert d["take_profit"] == 960.0
|
||||||
|
|
||||||
|
restored = Trade.from_dict(d)
|
||||||
|
assert restored.stop_loss == 720.0
|
||||||
|
assert restored.take_profit == 960.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_stop_loss_take_profit_default_none(sample_trade):
|
||||||
|
"""Trade defaults stop_loss and take_profit to None when not provided."""
|
||||||
|
assert sample_trade.stop_loss is None
|
||||||
|
assert sample_trade.take_profit is None
|
||||||
|
d = sample_trade.to_dict()
|
||||||
|
assert d["stop_loss"] is None
|
||||||
|
assert d["take_profit"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_from_dict_missing_risk_levels_defaults_none():
|
||||||
|
"""from_dict() gracefully handles missing stop_loss/take_profit keys."""
|
||||||
|
data = {
|
||||||
|
"trade_id": "t-1",
|
||||||
|
"portfolio_id": "p-1",
|
||||||
|
"ticker": "AAPL",
|
||||||
|
"action": "BUY",
|
||||||
|
"shares": 5.0,
|
||||||
|
"price": 150.0,
|
||||||
|
"total_value": 750.0,
|
||||||
|
}
|
||||||
|
trade = Trade.from_dict(data)
|
||||||
|
assert trade.stop_loss is None
|
||||||
|
assert trade.take_profit is None
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# PortfolioSnapshot round-trip
|
# PortfolioSnapshot round-trip
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ def test_execute_buy_success():
|
||||||
}
|
}
|
||||||
result = executor.execute_decisions("p1", decisions, PRICES)
|
result = executor.execute_decisions("p1", decisions, PRICES)
|
||||||
|
|
||||||
repo.add_holding.assert_called_once_with("p1", "MSFT", 10.0, 300.0, sector="Technology")
|
repo.add_holding.assert_called_once_with("p1", "MSFT", 10.0, 300.0, sector="Technology", stop_loss=None, take_profit=None)
|
||||||
assert len(result["executed_trades"]) == 1
|
assert len(result["executed_trades"]) == 1
|
||||||
assert result["executed_trades"][0]["action"] == "BUY"
|
assert result["executed_trades"][0]["action"] == "BUY"
|
||||||
|
|
||||||
|
|
@ -247,3 +247,56 @@ def test_execute_decisions_takes_snapshot():
|
||||||
repo.take_snapshot.assert_called_once_with("p1", PRICES)
|
repo.take_snapshot.assert_called_once_with("p1", PRICES)
|
||||||
assert "snapshot" in result
|
assert "snapshot" in result
|
||||||
assert result["snapshot"]["snapshot_id"] == "snap-1"
|
assert result["snapshot"]["snapshot_id"] == "snap-1"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stop loss / take profit tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_buy_with_stop_loss_and_take_profit():
|
||||||
|
"""BUY with stop_loss and take_profit passes them to add_holding and executed_trades."""
|
||||||
|
portfolio = _make_portfolio(cash=50_000.0, total_value=60_000.0)
|
||||||
|
repo = _make_repo(portfolio=portfolio)
|
||||||
|
executor = TradeExecutor(repo=repo, config=_DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
decisions = {
|
||||||
|
"sells": [],
|
||||||
|
"buys": [{
|
||||||
|
"ticker": "MSFT",
|
||||||
|
"shares": 5.0,
|
||||||
|
"sector": "Technology",
|
||||||
|
"rationale": "Breakout",
|
||||||
|
"stop_loss": 270.0,
|
||||||
|
"take_profit": 360.0,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
result = executor.execute_decisions("p1", decisions, PRICES)
|
||||||
|
|
||||||
|
repo.add_holding.assert_called_once_with(
|
||||||
|
"p1", "MSFT", 5.0, 300.0,
|
||||||
|
sector="Technology",
|
||||||
|
stop_loss=270.0,
|
||||||
|
take_profit=360.0,
|
||||||
|
)
|
||||||
|
assert len(result["executed_trades"]) == 1
|
||||||
|
trade = result["executed_trades"][0]
|
||||||
|
assert trade["stop_loss"] == 270.0
|
||||||
|
assert trade["take_profit"] == 360.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_buy_without_risk_levels_stores_none():
|
||||||
|
"""BUY with no stop_loss/take_profit stores None for both fields."""
|
||||||
|
portfolio = _make_portfolio(cash=50_000.0, total_value=60_000.0)
|
||||||
|
repo = _make_repo(portfolio=portfolio)
|
||||||
|
executor = TradeExecutor(repo=repo, config=_DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
decisions = {
|
||||||
|
"sells": [],
|
||||||
|
"buys": [{"ticker": "MSFT", "shares": 5.0, "sector": "Technology", "rationale": "Entry"}],
|
||||||
|
}
|
||||||
|
result = executor.execute_decisions("p1", decisions, PRICES)
|
||||||
|
|
||||||
|
trade = result["executed_trades"][0]
|
||||||
|
assert trade["stop_loss"] is None
|
||||||
|
assert trade["take_profit"] is None
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, patch
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
||||||
from langchain_core.runnables import Runnable
|
from langchain_core.runnables import Runnable
|
||||||
|
|
@ -7,6 +7,7 @@ from tradingagents.agents.analysts.market_analyst import create_market_analyst
|
||||||
from tradingagents.agents.analysts.social_media_analyst import create_social_media_analyst
|
from tradingagents.agents.analysts.social_media_analyst import create_social_media_analyst
|
||||||
from tradingagents.agents.analysts.news_analyst import create_news_analyst
|
from tradingagents.agents.analysts.news_analyst import create_news_analyst
|
||||||
|
|
||||||
|
|
||||||
class MockRunnable(Runnable):
|
class MockRunnable(Runnable):
|
||||||
def __init__(self, invoke_responses):
|
def __init__(self, invoke_responses):
|
||||||
self.invoke_responses = invoke_responses
|
self.invoke_responses = invoke_responses
|
||||||
|
|
@ -17,6 +18,7 @@ class MockRunnable(Runnable):
|
||||||
self.call_count += 1
|
self.call_count += 1
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class MockLLM(Runnable):
|
class MockLLM(Runnable):
|
||||||
def __init__(self, invoke_responses):
|
def __init__(self, invoke_responses):
|
||||||
self.runnable = MockRunnable(invoke_responses)
|
self.runnable = MockRunnable(invoke_responses)
|
||||||
|
|
@ -29,6 +31,7 @@ class MockLLM(Runnable):
|
||||||
self.tools_bound = tools
|
self.tools_bound = tools
|
||||||
return self.runnable
|
return self.runnable
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_state():
|
def mock_state():
|
||||||
return {
|
return {
|
||||||
|
|
@ -37,8 +40,10 @@ def mock_state():
|
||||||
"company_of_interest": "AAPL",
|
"company_of_interest": "AAPL",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_llm_with_tool_call():
|
def mock_llm_with_tool_call():
|
||||||
|
"""LLM that makes one tool call then writes the final report (iterative loop)."""
|
||||||
# 1. First call: The LLM decides to use a tool
|
# 1. First call: The LLM decides to use a tool
|
||||||
tool_call_msg = AIMessage(
|
tool_call_msg = AIMessage(
|
||||||
content="",
|
content="",
|
||||||
|
|
@ -52,22 +57,108 @@ def mock_llm_with_tool_call():
|
||||||
)
|
)
|
||||||
return MockLLM([tool_call_msg, final_report_msg])
|
return MockLLM([tool_call_msg, final_report_msg])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_llm_direct_report():
|
||||||
|
"""LLM that returns the final report directly (no tool calls — full pre-fetch path)."""
|
||||||
|
final_report_msg = AIMessage(
|
||||||
|
content="This is the final report after running the tool."
|
||||||
|
)
|
||||||
|
return MockLLM([final_report_msg])
|
||||||
|
|
||||||
|
|
||||||
def test_fundamentals_analyst_tool_loop(mock_state, mock_llm_with_tool_call):
|
def test_fundamentals_analyst_tool_loop(mock_state, mock_llm_with_tool_call):
|
||||||
|
"""Fundamentals analyst: pre-fetches 4 tools, runs iterative loop for raw statements."""
|
||||||
node = create_fundamentals_analyst(mock_llm_with_tool_call)
|
node = create_fundamentals_analyst(mock_llm_with_tool_call)
|
||||||
result = node(mock_state)
|
result = node(mock_state)
|
||||||
assert "This is the final report after running the tool." in result["fundamentals_report"]
|
assert "This is the final report after running the tool." in result["fundamentals_report"]
|
||||||
|
|
||||||
|
|
||||||
def test_market_analyst_tool_loop(mock_state, mock_llm_with_tool_call):
|
def test_market_analyst_tool_loop(mock_state, mock_llm_with_tool_call):
|
||||||
|
"""Market analyst: pre-fetches macro + stock data, keeps indicator selection iterative."""
|
||||||
node = create_market_analyst(mock_llm_with_tool_call)
|
node = create_market_analyst(mock_llm_with_tool_call)
|
||||||
result = node(mock_state)
|
result = node(mock_state)
|
||||||
assert "This is the final report after running the tool." in result["market_report"]
|
assert "This is the final report after running the tool." in result["market_report"]
|
||||||
|
|
||||||
def test_social_media_analyst_tool_loop(mock_state, mock_llm_with_tool_call):
|
|
||||||
node = create_social_media_analyst(mock_llm_with_tool_call)
|
def test_social_media_analyst_direct_invoke(mock_state, mock_llm_direct_report):
|
||||||
|
"""Social analyst: full pre-fetch, direct LLM invoke (no tool loop)."""
|
||||||
|
node = create_social_media_analyst(mock_llm_direct_report)
|
||||||
result = node(mock_state)
|
result = node(mock_state)
|
||||||
assert "This is the final report after running the tool." in result["sentiment_report"]
|
assert "This is the final report after running the tool." in result["sentiment_report"]
|
||||||
|
|
||||||
def test_news_analyst_tool_loop(mock_state, mock_llm_with_tool_call):
|
|
||||||
node = create_news_analyst(mock_llm_with_tool_call)
|
def test_news_analyst_direct_invoke(mock_state, mock_llm_direct_report):
|
||||||
|
"""News analyst: full pre-fetch, direct LLM invoke (no tool loop)."""
|
||||||
|
node = create_news_analyst(mock_llm_direct_report)
|
||||||
result = node(mock_state)
|
result = node(mock_state)
|
||||||
assert "This is the final report after running the tool." in result["news_report"]
|
assert "This is the final report after running the tool." in result["news_report"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_market_analyst_macro_regime_from_prefetch(mock_state, mock_llm_with_tool_call):
|
||||||
|
"""Market analyst populates macro_regime_report from pre-fetched data when available."""
|
||||||
|
with patch(
|
||||||
|
"tradingagents.agents.analysts.market_analyst.prefetch_tools_parallel",
|
||||||
|
return_value={
|
||||||
|
"Macro Regime Classification": "## Risk-On\nMarket is RISK-ON.",
|
||||||
|
"Stock Price Data": "Date,Close\n2024-05-14,189.0",
|
||||||
|
},
|
||||||
|
):
|
||||||
|
node = create_market_analyst(mock_llm_with_tool_call)
|
||||||
|
result = node(mock_state)
|
||||||
|
assert result["macro_regime_report"] == "## Risk-On\nMarket is RISK-ON."
|
||||||
|
|
||||||
|
|
||||||
|
def test_social_media_analyst_no_bind_tools(mock_state, mock_llm_direct_report):
|
||||||
|
"""Social analyst must not call bind_tools since there are no tools."""
|
||||||
|
node = create_social_media_analyst(mock_llm_direct_report)
|
||||||
|
node(mock_state)
|
||||||
|
# bind_tools should never have been called (no tools in the list)
|
||||||
|
assert mock_llm_direct_report.tools_bound is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_prefetched_context_injected_into_prompt(mock_state, mock_llm_with_tool_call):
|
||||||
|
"""Market analyst injects pre-fetched context into the prompt sent to the LLM."""
|
||||||
|
captured_inputs = []
|
||||||
|
|
||||||
|
class CapturingRunnable(Runnable):
|
||||||
|
def invoke(self, input, config=None, **kwargs):
|
||||||
|
captured_inputs.append(input)
|
||||||
|
# Return final report directly to end the loop early
|
||||||
|
return AIMessage(content="This is the final report after running the tool.")
|
||||||
|
|
||||||
|
class CapturingLLM(Runnable):
|
||||||
|
def invoke(self, input, config=None, **kwargs):
|
||||||
|
captured_inputs.append(input)
|
||||||
|
return AIMessage(content="This is the final report after running the tool.")
|
||||||
|
|
||||||
|
def bind_tools(self, tools):
|
||||||
|
return CapturingRunnable()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"tradingagents.agents.analysts.market_analyst.prefetch_tools_parallel",
|
||||||
|
return_value={
|
||||||
|
"Macro Regime Classification": "**RISK-ON** regime detected.",
|
||||||
|
"Stock Price Data": "Date,Close\n2024-05-14,189.0",
|
||||||
|
},
|
||||||
|
):
|
||||||
|
node = create_market_analyst(CapturingLLM())
|
||||||
|
node(mock_state)
|
||||||
|
|
||||||
|
# The prompt was captured; find the system message and verify injected context
|
||||||
|
assert captured_inputs, "LLM was never called"
|
||||||
|
# The input to the runnable is a list of messages; find the system message text
|
||||||
|
messages = captured_inputs[0]
|
||||||
|
full_text = " ".join(
|
||||||
|
m.content if hasattr(m, "content") else str(m)
|
||||||
|
for m in messages
|
||||||
|
)
|
||||||
|
assert "RISK-ON" in full_text
|
||||||
|
assert "Pre-loaded Context" in full_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_news_analyst_no_bind_tools(mock_state, mock_llm_direct_report):
|
||||||
|
"""News analyst must not call bind_tools since there are no tools."""
|
||||||
|
node = create_news_analyst(mock_llm_direct_report)
|
||||||
|
node(mock_state)
|
||||||
|
assert mock_llm_direct_report.tools_bound is None
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,100 @@
|
||||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||||
import time
|
|
||||||
import json
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
|
build_instrument_context,
|
||||||
|
format_prefetched_context,
|
||||||
|
prefetch_tools_parallel,
|
||||||
|
)
|
||||||
from tradingagents.agents.utils.fundamental_data_tools import (
|
from tradingagents.agents.utils.fundamental_data_tools import (
|
||||||
get_fundamentals,
|
|
||||||
get_balance_sheet,
|
get_balance_sheet,
|
||||||
get_cashflow,
|
get_cashflow,
|
||||||
|
get_fundamentals,
|
||||||
get_income_statement,
|
get_income_statement,
|
||||||
get_ttm_analysis,
|
|
||||||
get_peer_comparison,
|
get_peer_comparison,
|
||||||
get_sector_relative,
|
get_sector_relative,
|
||||||
|
get_ttm_analysis,
|
||||||
)
|
)
|
||||||
from tradingagents.agents.utils.news_data_tools import get_insider_transactions
|
from tradingagents.agents.utils.news_data_tools import get_insider_transactions
|
||||||
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
||||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
|
||||||
from tradingagents.dataflows.config import get_config
|
from tradingagents.dataflows.config import get_config
|
||||||
|
|
||||||
|
|
||||||
def create_fundamentals_analyst(llm):
|
def create_fundamentals_analyst(llm):
|
||||||
def fundamentals_analyst_node(state):
|
def fundamentals_analyst_node(state):
|
||||||
current_date = state["trade_date"]
|
current_date = state["trade_date"]
|
||||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
ticker = state["company_of_interest"]
|
||||||
|
instrument_context = build_instrument_context(ticker)
|
||||||
|
|
||||||
tools = [
|
# ── Pre-fetch the four mandatory foundational datasets in parallel ────
|
||||||
get_ttm_analysis,
|
# get_ttm_analysis, get_fundamentals, get_peer_comparison, and
|
||||||
get_fundamentals,
|
# get_sector_relative are always called — pre-fetching them removes
|
||||||
get_balance_sheet,
|
# 4 LLM round-trips. The raw financial statements (balance sheet,
|
||||||
get_cashflow,
|
# cashflow, income statement) stay iterative: the LLM may request them
|
||||||
get_income_statement,
|
# only if it spots anomalies worth investigating in the pre-loaded data.
|
||||||
get_peer_comparison,
|
prefetched = prefetch_tools_parallel(
|
||||||
get_sector_relative,
|
[
|
||||||
|
{
|
||||||
|
"tool": get_ttm_analysis,
|
||||||
|
"args": {"ticker": ticker, "curr_date": current_date},
|
||||||
|
"label": "TTM Analysis (8-Quarter Trend)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": get_fundamentals,
|
||||||
|
"args": {"ticker": ticker, "curr_date": current_date},
|
||||||
|
"label": "Fundamental Ratios Snapshot",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": get_peer_comparison,
|
||||||
|
"args": {"ticker": ticker, "curr_date": current_date},
|
||||||
|
"label": "Peer Comparison",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": get_sector_relative,
|
||||||
|
"args": {"ticker": ticker, "curr_date": current_date},
|
||||||
|
"label": "Sector Relative Performance",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
prefetched_context = format_prefetched_context(prefetched)
|
||||||
|
|
||||||
|
# ── Only the raw statement tools remain iterative ─────────────────────
|
||||||
|
tools = [get_balance_sheet, get_cashflow, get_income_statement]
|
||||||
|
|
||||||
system_message = (
|
system_message = (
|
||||||
"You are a researcher tasked with performing deep fundamental analysis of a company over the last 8 quarters (2 years) to support medium-term investment decisions."
|
"You are a researcher tasked with performing deep fundamental analysis of a company "
|
||||||
" Follow this sequence:"
|
"over the last 8 quarters (2 years) to support medium-term investment decisions.\n\n"
|
||||||
" 1. Call `get_ttm_analysis` first — this provides a Trailing Twelve Months (TTM) trend report covering revenue growth (QoQ and YoY), margin trajectories (gross, operating, net), return on equity trend, debt/equity trend, and free cash flow over 8 quarters."
|
"## Pre-loaded Foundational Data\n\n"
|
||||||
" 2. Call `get_fundamentals` for the latest snapshot of key ratios (PE, PEG, price-to-book, beta, 52-week range)."
|
"The following datasets have already been fetched and are provided in the "
|
||||||
" 3. Call `get_peer_comparison` to see how the company ranks against sector peers over 1-week, 1-month, 3-month, and 6-month periods."
|
"**Pre-loaded Context** section below. Do NOT call `get_ttm_analysis`, "
|
||||||
" 4. Call `get_sector_relative` to compute the company's alpha vs its sector ETF benchmark."
|
"`get_fundamentals`, `get_peer_comparison`, or `get_sector_relative` — "
|
||||||
" 5. Optionally call `get_balance_sheet`, `get_cashflow`, or `get_income_statement` for additional detail."
|
"that data is already available:\n\n"
|
||||||
" Write a comprehensive report covering: multi-quarter revenue and margin trends, TTM metrics, relative valuation vs peers, sector outperformance or underperformance, and a clear medium-term fundamental thesis."
|
"- **TTM Analysis**: 8-quarter Trailing Twelve Months trends — revenue growth "
|
||||||
" Do not simply state trends are mixed — provide detailed, fine-grained analysis that identifies inflection points, acceleration or deceleration in growth, and specific risks and opportunities."
|
"(QoQ and YoY), margin trajectories (gross, operating, net), ROE trend, "
|
||||||
" Make sure to append a Markdown summary table at the end of the report organising key metrics for easy reference.",
|
"debt/equity trend, and free cash flow.\n"
|
||||||
|
"- **Fundamental Ratios**: Latest snapshot of key ratios (PE, PEG, price-to-book, "
|
||||||
|
"beta, 52-week range).\n"
|
||||||
|
"- **Peer Comparison**: How the company ranks against sector peers over 1-week, "
|
||||||
|
"1-month, 3-month, and 6-month periods.\n"
|
||||||
|
"- **Sector Relative Performance**: The company's alpha vs its sector ETF benchmark.\n\n"
|
||||||
|
"## Your Task\n\n"
|
||||||
|
"Interpret the pre-loaded data analytically. Look for:\n"
|
||||||
|
"- Revenue and margin inflection points — acceleration, deceleration, or trend reversals\n"
|
||||||
|
"- Suspicious deviations in FCF vs reported net income (earnings quality signals)\n"
|
||||||
|
"- Peer divergence — is the company outperforming or underperforming its sector?\n"
|
||||||
|
"- Valuation anomalies vs growth trajectory (PEG vs actual growth rate)\n\n"
|
||||||
|
"If you identify anything suspicious in the TTM or fundamentals data that warrants "
|
||||||
|
"deeper investigation — for example, a margin inflection without an obvious revenue "
|
||||||
|
"driver, an FCF deviation from net income, or an unusual balance-sheet move — you "
|
||||||
|
"may call `get_balance_sheet`, `get_cashflow`, or `get_income_statement` to examine "
|
||||||
|
"the raw quarterly data directly.\n\n"
|
||||||
|
"Write a comprehensive report covering: multi-quarter revenue and margin trends, "
|
||||||
|
"TTM metrics, relative valuation vs peers, sector outperformance or underperformance, "
|
||||||
|
"and a clear medium-term fundamental thesis. "
|
||||||
|
"Do not simply state trends are mixed — provide detailed, fine-grained analysis that "
|
||||||
|
"identifies inflection points, acceleration or deceleration in growth, and specific "
|
||||||
|
"risks and opportunities. "
|
||||||
|
"Make sure to append a Markdown summary table at the end of the report organising "
|
||||||
|
"key metrics for easy reference."
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = ChatPromptTemplate.from_messages(
|
prompt = ChatPromptTemplate.from_messages(
|
||||||
|
|
@ -55,7 +108,8 @@ def create_fundamentals_analyst(llm):
|
||||||
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
||||||
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
||||||
" You have access to the following tools: {tool_names}.\n{system_message}"
|
" You have access to the following tools: {tool_names}.\n{system_message}"
|
||||||
"For your reference, the current date is {current_date}. {instrument_context}",
|
"For your reference, the current date is {current_date}. {instrument_context}\n\n"
|
||||||
|
"## Pre-loaded Context\n\n{prefetched_context}",
|
||||||
),
|
),
|
||||||
MessagesPlaceholder(variable_name="messages"),
|
MessagesPlaceholder(variable_name="messages"),
|
||||||
]
|
]
|
||||||
|
|
@ -65,6 +119,7 @@ def create_fundamentals_analyst(llm):
|
||||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||||
prompt = prompt.partial(current_date=current_date)
|
prompt = prompt.partial(current_date=current_date)
|
||||||
prompt = prompt.partial(instrument_context=instrument_context)
|
prompt = prompt.partial(instrument_context=instrument_context)
|
||||||
|
prompt = prompt.partial(prefetched_context=prefetched_context)
|
||||||
|
|
||||||
chain = prompt | llm.bind_tools(tools)
|
chain = prompt | llm.bind_tools(tools)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||||
import time
|
|
||||||
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
|
build_instrument_context,
|
||||||
|
format_prefetched_context,
|
||||||
|
prefetch_tools_parallel,
|
||||||
|
)
|
||||||
from tradingagents.agents.utils.core_stock_tools import get_stock_data
|
from tradingagents.agents.utils.core_stock_tools import get_stock_data
|
||||||
from tradingagents.agents.utils.technical_indicators_tools import get_indicators
|
|
||||||
from tradingagents.agents.utils.fundamental_data_tools import get_macro_regime
|
from tradingagents.agents.utils.fundamental_data_tools import get_macro_regime
|
||||||
|
from tradingagents.agents.utils.technical_indicators_tools import get_indicators
|
||||||
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
||||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
|
||||||
from tradingagents.dataflows.config import get_config
|
from tradingagents.dataflows.config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,43 +18,107 @@ def create_market_analyst(llm):
|
||||||
|
|
||||||
def market_analyst_node(state):
|
def market_analyst_node(state):
|
||||||
current_date = state["trade_date"]
|
current_date = state["trade_date"]
|
||||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
ticker = state["company_of_interest"]
|
||||||
|
instrument_context = build_instrument_context(ticker)
|
||||||
|
|
||||||
tools = [
|
# ── Pre-fetch macro regime and stock price data in parallel ──────────
|
||||||
get_macro_regime,
|
# Both are always required; fetching them upfront removes 2 LLM round-
|
||||||
get_stock_data,
|
# trips and lets the LLM focus its single tool-call budget on choosing
|
||||||
get_indicators,
|
# the right indicators based on the macro regime it sees.
|
||||||
|
trade_date = datetime.strptime(current_date, "%Y-%m-%d")
|
||||||
|
stock_start = (trade_date - timedelta(days=365)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
prefetched = prefetch_tools_parallel(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"tool": get_macro_regime,
|
||||||
|
"args": {"curr_date": current_date},
|
||||||
|
"label": "Macro Regime Classification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": get_stock_data,
|
||||||
|
"args": {
|
||||||
|
"symbol": ticker,
|
||||||
|
"start_date": stock_start,
|
||||||
|
"end_date": current_date,
|
||||||
|
},
|
||||||
|
"label": "Stock Price Data",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
prefetched_context = format_prefetched_context(prefetched)
|
||||||
|
|
||||||
|
# ── Only get_indicators remains iterative ─────────────────────────────
|
||||||
|
# The LLM reads the macro regime from the pre-loaded context and decides
|
||||||
|
# which indicators are most relevant before calling get_indicators.
|
||||||
|
tools = [get_indicators]
|
||||||
|
|
||||||
system_message = (
|
system_message = (
|
||||||
"""You are a trading assistant tasked with analyzing financial markets. Start by calling `get_macro_regime` to classify the current macro environment as risk-on, risk-off, or transition. Use this macro context to frame all subsequent technical analysis — for example, in risk-off environments weight bearish signals more heavily, and in risk-on environments favour momentum and breakout signals.
|
"You are a trading assistant tasked with analyzing financial markets.\n\n"
|
||||||
|
"## Pre-loaded Data\n\n"
|
||||||
Then, select the **most relevant indicators** for the given market condition from the following list. The goal is to choose up to **8 indicators** that provide complementary insights without redundancy. Categories and each category's indicators are:
|
"The macro regime classification and recent stock price data for the company under "
|
||||||
|
"analysis have already been fetched and are provided in the **Pre-loaded Context** "
|
||||||
Moving Averages:
|
"section below. "
|
||||||
- close_50_sma: 50 SMA: A medium-term trend indicator. Usage: Identify trend direction and serve as dynamic support/resistance. Tips: It lags price; combine with faster indicators for timely signals.
|
"Do NOT call `get_macro_regime` or `get_stock_data` — the data is already available.\n\n"
|
||||||
- close_200_sma: 200 SMA: A long-term trend benchmark. Usage: Confirm overall market trend and identify golden/death cross setups. Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries.
|
"## Your Task\n\n"
|
||||||
- close_10_ema: 10 EMA: A responsive short-term average. Usage: Capture quick shifts in momentum and potential entry points. Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals.
|
"1. Read the macro regime classification from the pre-loaded context. "
|
||||||
|
"The macro regime has been classified above — use it to weight your indicator "
|
||||||
MACD Related:
|
"choices before calling `get_indicators`. For example, in risk-off environments "
|
||||||
- macd: MACD: Computes momentum via differences of EMAs. Usage: Look for crossovers and divergence as signals of trend changes. Tips: Confirm with other indicators in low-volatility or sideways markets.
|
"favour ATR, Bollinger Bands, and long-term SMAs; in risk-on environments favour "
|
||||||
- macds: MACD Signal: An EMA smoothing of the MACD line. Usage: Use crossovers with the MACD line to trigger trades. Tips: Should be part of a broader strategy to avoid false positives.
|
"momentum indicators like MACD and short EMAs.\n\n"
|
||||||
- macdh: MACD Histogram: Shows the gap between the MACD line and its signal. Usage: Visualize momentum strength and spot divergence early. Tips: Can be volatile; complement with additional filters in fast-moving markets.
|
"2. Select the **most relevant indicators** for the given market condition from "
|
||||||
|
"the list below. Choose up to **8 indicators** that provide complementary insights "
|
||||||
Momentum Indicators:
|
"without redundancy.\n\n"
|
||||||
- rsi: RSI: Measures momentum to flag overbought/oversold conditions. Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis.
|
"Moving Averages:\n"
|
||||||
|
"- close_50_sma: 50 SMA: A medium-term trend indicator. Usage: Identify trend "
|
||||||
Volatility Indicators:
|
"direction and serve as dynamic support/resistance. Tips: It lags price; combine "
|
||||||
- boll: Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. Usage: Acts as a dynamic benchmark for price movement. Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals.
|
"with faster indicators for timely signals.\n"
|
||||||
- boll_ub: Bollinger Upper Band: Typically 2 standard deviations above the middle line. Usage: Signals potential overbought conditions and breakout zones. Tips: Confirm signals with other tools; prices may ride the band in strong trends.
|
"- close_200_sma: 200 SMA: A long-term trend benchmark. Usage: Confirm overall "
|
||||||
- boll_lb: Bollinger Lower Band: Typically 2 standard deviations below the middle line. Usage: Indicates potential oversold conditions. Tips: Use additional analysis to avoid false reversal signals.
|
"market trend and identify golden/death cross setups. Tips: It reacts slowly; best "
|
||||||
- atr: ATR: Averages true range to measure volatility. Usage: Set stop-loss levels and adjust position sizes based on current market volatility. Tips: It's a reactive measure, so use it as part of a broader risk management strategy.
|
"for strategic trend confirmation rather than frequent trading entries.\n"
|
||||||
|
"- close_10_ema: 10 EMA: A responsive short-term average. Usage: Capture quick "
|
||||||
Volume-Based Indicators:
|
"shifts in momentum and potential entry points. Tips: Prone to noise in choppy "
|
||||||
- vwma: VWMA: A moving average weighted by volume. Usage: Confirm trends by integrating price action with volume data. Tips: Watch for skewed results from volume spikes; use in combination with other volume analyses.
|
"markets; use alongside longer averages for filtering false signals.\n\n"
|
||||||
|
"MACD Related:\n"
|
||||||
- Select indicators that provide diverse and complementary information. Avoid redundancy (e.g., do not select both rsi and stochrsi). Also briefly explain why they are suitable for the given market context. When you tool call, please use the exact name of the indicators provided above as they are defined parameters, otherwise your call will fail. Please make sure to call get_stock_data first to retrieve the CSV that is needed to generate indicators. Then use get_indicators with the specific indicator names. Write a very detailed and nuanced report of the trends you observe. Provide specific, actionable insights with supporting evidence to help traders make informed decisions."""
|
"- macd: MACD: Computes momentum via differences of EMAs. Usage: Look for "
|
||||||
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
|
"crossovers and divergence as signals of trend changes. Tips: Confirm with other "
|
||||||
|
"indicators in low-volatility or sideways markets.\n"
|
||||||
|
"- macds: MACD Signal: An EMA smoothing of the MACD line. Usage: Use crossovers "
|
||||||
|
"with the MACD line to trigger trades. Tips: Should be part of a broader strategy "
|
||||||
|
"to avoid false positives.\n"
|
||||||
|
"- macdh: MACD Histogram: Shows the gap between the MACD line and its signal. "
|
||||||
|
"Usage: Visualize momentum strength and spot divergence early. Tips: Can be "
|
||||||
|
"volatile; complement with additional filters in fast-moving markets.\n\n"
|
||||||
|
"Momentum Indicators:\n"
|
||||||
|
"- rsi: RSI: Measures momentum to flag overbought/oversold conditions. Usage: "
|
||||||
|
"Apply 70/30 thresholds and watch for divergence to signal reversals. Tips: In "
|
||||||
|
"strong trends, RSI may remain extreme; always cross-check with trend analysis.\n\n"
|
||||||
|
"Volatility Indicators:\n"
|
||||||
|
"- boll: Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. "
|
||||||
|
"Usage: Acts as a dynamic benchmark for price movement. Tips: Combine with the "
|
||||||
|
"upper and lower bands to effectively spot breakouts or reversals.\n"
|
||||||
|
"- boll_ub: Bollinger Upper Band: Typically 2 standard deviations above the middle "
|
||||||
|
"line. Usage: Signals potential overbought conditions and breakout zones. Tips: "
|
||||||
|
"Confirm signals with other tools; prices may ride the band in strong trends.\n"
|
||||||
|
"- boll_lb: Bollinger Lower Band: Typically 2 standard deviations below the middle "
|
||||||
|
"line. Usage: Indicates potential oversold conditions. Tips: Use additional "
|
||||||
|
"analysis to avoid false reversal signals.\n"
|
||||||
|
"- atr: ATR: Averages true range to measure volatility. Usage: Set stop-loss "
|
||||||
|
"levels and adjust position sizes based on current market volatility. Tips: It's "
|
||||||
|
"a reactive measure, so use it as part of a broader risk management strategy.\n\n"
|
||||||
|
"Volume-Based Indicators:\n"
|
||||||
|
"- vwma: VWMA: A moving average weighted by volume. Usage: Confirm trends by "
|
||||||
|
"integrating price action with volume data. Tips: Watch for skewed results from "
|
||||||
|
"volume spikes; use in combination with other volume analyses.\n\n"
|
||||||
|
"3. Select indicators that provide diverse and complementary information. Avoid "
|
||||||
|
"redundancy (e.g., do not select both rsi and stochrsi). Briefly explain why each "
|
||||||
|
"chosen indicator is suitable for the current macro context. When calling "
|
||||||
|
"`get_indicators`, use the exact indicator names listed above — they are defined "
|
||||||
|
"parameters and any deviation will cause the call to fail.\n\n"
|
||||||
|
"4. Write a very detailed and nuanced report of the trends you observe. Provide "
|
||||||
|
"specific, actionable insights with supporting evidence to help traders make "
|
||||||
|
"informed decisions. Make sure to append a Markdown table at the end of the report "
|
||||||
|
"to organise key points, making it easy to read."
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = ChatPromptTemplate.from_messages(
|
prompt = ChatPromptTemplate.from_messages(
|
||||||
|
|
@ -62,7 +132,8 @@ Volume-Based Indicators:
|
||||||
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
||||||
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
||||||
" You have access to the following tools: {tool_names}.\n{system_message}"
|
" You have access to the following tools: {tool_names}.\n{system_message}"
|
||||||
"For your reference, the current date is {current_date}. {instrument_context}",
|
"For your reference, the current date is {current_date}. {instrument_context}\n\n"
|
||||||
|
"## Pre-loaded Context\n\n{prefetched_context}",
|
||||||
),
|
),
|
||||||
MessagesPlaceholder(variable_name="messages"),
|
MessagesPlaceholder(variable_name="messages"),
|
||||||
]
|
]
|
||||||
|
|
@ -72,6 +143,7 @@ Volume-Based Indicators:
|
||||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
||||||
prompt = prompt.partial(current_date=current_date)
|
prompt = prompt.partial(current_date=current_date)
|
||||||
prompt = prompt.partial(instrument_context=instrument_context)
|
prompt = prompt.partial(instrument_context=instrument_context)
|
||||||
|
prompt = prompt.partial(prefetched_context=prefetched_context)
|
||||||
|
|
||||||
chain = prompt | llm.bind_tools(tools)
|
chain = prompt | llm.bind_tools(tools)
|
||||||
|
|
||||||
|
|
@ -80,8 +152,16 @@ Volume-Based Indicators:
|
||||||
report = result.content or ""
|
report = result.content or ""
|
||||||
macro_regime_report = ""
|
macro_regime_report = ""
|
||||||
|
|
||||||
# Extract macro regime section if present
|
# Extract macro regime section if present (from pre-loaded context or report)
|
||||||
if report and ("Macro Regime Classification" in report or "RISK-ON" in report.upper() or "RISK-OFF" in report.upper() or "TRANSITION" in report.upper()):
|
regime_data = prefetched.get("Macro Regime Classification", "")
|
||||||
|
if regime_data and not regime_data.startswith("[Error"):
|
||||||
|
macro_regime_report = regime_data
|
||||||
|
elif report and (
|
||||||
|
"Macro Regime Classification" in report
|
||||||
|
or "RISK-ON" in report.upper()
|
||||||
|
or "RISK-OFF" in report.upper()
|
||||||
|
or "TRANSITION" in report.upper()
|
||||||
|
):
|
||||||
macro_regime_report = report
|
macro_regime_report = report
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,65 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||||
import json
|
|
||||||
from tradingagents.agents.utils.news_data_tools import get_news, get_global_news
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
build_instrument_context,
|
||||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
format_prefetched_context,
|
||||||
|
prefetch_tools_parallel,
|
||||||
|
)
|
||||||
|
from tradingagents.agents.utils.news_data_tools import get_global_news, get_news
|
||||||
from tradingagents.dataflows.config import get_config
|
from tradingagents.dataflows.config import get_config
|
||||||
|
|
||||||
|
|
||||||
def create_news_analyst(llm):
|
def create_news_analyst(llm):
|
||||||
def news_analyst_node(state):
|
def news_analyst_node(state):
|
||||||
current_date = state["trade_date"]
|
current_date = state["trade_date"]
|
||||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
ticker = state["company_of_interest"]
|
||||||
|
instrument_context = build_instrument_context(ticker)
|
||||||
|
|
||||||
tools = [
|
# ── Pre-fetch company-specific and global news in parallel ────────────
|
||||||
get_news,
|
trade_date = datetime.strptime(current_date, "%Y-%m-%d")
|
||||||
get_global_news,
|
start_date = (trade_date - timedelta(days=7)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
prefetched = prefetch_tools_parallel(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"tool": get_news,
|
||||||
|
"args": {
|
||||||
|
"ticker": ticker,
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": current_date,
|
||||||
|
},
|
||||||
|
"label": "Company-Specific News (Last 7 Days)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tool": get_global_news,
|
||||||
|
"args": {
|
||||||
|
"curr_date": current_date,
|
||||||
|
"look_back_days": 7,
|
||||||
|
"limit": 5,
|
||||||
|
},
|
||||||
|
"label": "Global Macroeconomic News (Last 7 Days)",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
prefetched_context = format_prefetched_context(prefetched)
|
||||||
|
|
||||||
system_message = (
|
system_message = (
|
||||||
"You are a news researcher tasked with analyzing recent news and trends over the past week. Please write a comprehensive report of the current state of the world that is relevant for trading and macroeconomics. Use the available tools: get_news(query, start_date, end_date) for company-specific or targeted news searches, and get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions."
|
"You are a news researcher tasked with analyzing recent news and trends over "
|
||||||
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
|
"the past week.\n\n"
|
||||||
|
"## Pre-loaded Data\n\n"
|
||||||
|
"Both company-specific news and global macroeconomic news for the past 7 days "
|
||||||
|
"have already been fetched and are provided in the **Pre-loaded Context** section "
|
||||||
|
"below. Do NOT call `get_news` or `get_global_news` — the data is already available.\n\n"
|
||||||
|
"## Your Task\n\n"
|
||||||
|
"Synthesize the pre-loaded news feeds into a comprehensive report covering the "
|
||||||
|
"current state of the world as it is relevant to trading and macroeconomics. "
|
||||||
|
"Cross-reference company-specific developments with the broader macro backdrop. "
|
||||||
|
"Provide specific, actionable insights with supporting evidence to help traders "
|
||||||
|
"make informed decisions. "
|
||||||
|
"Make sure to append a Markdown table at the end of the report to organise key "
|
||||||
|
"points, making it easy to read."
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = ChatPromptTemplate.from_messages(
|
prompt = ChatPromptTemplate.from_messages(
|
||||||
|
|
@ -26,25 +67,27 @@ def create_news_analyst(llm):
|
||||||
(
|
(
|
||||||
"system",
|
"system",
|
||||||
"You are a helpful AI assistant, collaborating with other assistants."
|
"You are a helpful AI assistant, collaborating with other assistants."
|
||||||
" Use the provided tools to progress towards answering the question."
|
|
||||||
" If you are unable to fully answer, that's OK; another assistant with different tools"
|
" If you are unable to fully answer, that's OK; another assistant with different tools"
|
||||||
" will help where you left off. Execute what you can to make progress."
|
" will help where you left off. Execute what you can to make progress."
|
||||||
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
||||||
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
||||||
" You have access to the following tools: {tool_names}.\n{system_message}"
|
"\n{system_message}"
|
||||||
"For your reference, the current date is {current_date}. {instrument_context}",
|
"For your reference, the current date is {current_date}. {instrument_context}\n\n"
|
||||||
|
"## Pre-loaded Context\n\n{prefetched_context}",
|
||||||
),
|
),
|
||||||
MessagesPlaceholder(variable_name="messages"),
|
MessagesPlaceholder(variable_name="messages"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = prompt.partial(system_message=system_message)
|
prompt = prompt.partial(system_message=system_message)
|
||||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
|
||||||
prompt = prompt.partial(current_date=current_date)
|
prompt = prompt.partial(current_date=current_date)
|
||||||
prompt = prompt.partial(instrument_context=instrument_context)
|
prompt = prompt.partial(instrument_context=instrument_context)
|
||||||
|
prompt = prompt.partial(prefetched_context=prefetched_context)
|
||||||
|
|
||||||
chain = prompt | llm.bind_tools(tools)
|
# No tools remain — use direct invocation (no bind_tools, no tool loop)
|
||||||
result = run_tool_loop(chain, state["messages"], tools)
|
chain = prompt | llm
|
||||||
|
|
||||||
|
result = chain.invoke(state["messages"])
|
||||||
|
|
||||||
report = result.content or ""
|
report = result.content or ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,59 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||||
import time
|
|
||||||
import json
|
from tradingagents.agents.utils.agent_utils import (
|
||||||
|
build_instrument_context,
|
||||||
|
format_prefetched_context,
|
||||||
|
prefetch_tools_parallel,
|
||||||
|
)
|
||||||
from tradingagents.agents.utils.news_data_tools import get_news
|
from tradingagents.agents.utils.news_data_tools import get_news
|
||||||
from tradingagents.agents.utils.tool_runner import run_tool_loop
|
|
||||||
from tradingagents.agents.utils.agent_utils import build_instrument_context
|
|
||||||
from tradingagents.dataflows.config import get_config
|
from tradingagents.dataflows.config import get_config
|
||||||
|
|
||||||
|
|
||||||
def create_social_media_analyst(llm):
|
def create_social_media_analyst(llm):
|
||||||
def social_media_analyst_node(state):
|
def social_media_analyst_node(state):
|
||||||
current_date = state["trade_date"]
|
current_date = state["trade_date"]
|
||||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
ticker = state["company_of_interest"]
|
||||||
|
instrument_context = build_instrument_context(ticker)
|
||||||
|
|
||||||
tools = [
|
# ── Pre-fetch company news for the past 7 days ────────────────────────
|
||||||
get_news,
|
trade_date = datetime.strptime(current_date, "%Y-%m-%d")
|
||||||
|
start_date = (trade_date - timedelta(days=7)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
prefetched = prefetch_tools_parallel(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"tool": get_news,
|
||||||
|
"args": {
|
||||||
|
"ticker": ticker,
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": current_date,
|
||||||
|
},
|
||||||
|
"label": "Company News & Social Media (Last 7 Days)",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
prefetched_context = format_prefetched_context(prefetched)
|
||||||
|
|
||||||
system_message = (
|
system_message = (
|
||||||
"You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_news(query, start_date, end_date) tool to search for company-specific news and social media discussions. Try to look at all sources possible from social media to sentiment to news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions."
|
"You are a social media and company-specific news researcher/analyst tasked with "
|
||||||
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
|
"analyzing social media posts, recent company news, and public sentiment for a "
|
||||||
|
"specific company over the past week.\n\n"
|
||||||
|
"## Pre-loaded Data\n\n"
|
||||||
|
"Company-specific news and social media discussions for the past 7 days have already "
|
||||||
|
"been fetched and are provided in the **Pre-loaded Context** section below. "
|
||||||
|
"Do NOT call `get_news` — the data is already available.\n\n"
|
||||||
|
"## Your Task\n\n"
|
||||||
|
"Using the pre-loaded news and social media data, write a comprehensive long report "
|
||||||
|
"detailing your analysis, insights, and implications for traders and investors on "
|
||||||
|
"this company's current state. Cover:\n"
|
||||||
|
"- Social media sentiment and what people are saying about the company\n"
|
||||||
|
"- Daily sentiment shifts over the past week\n"
|
||||||
|
"- Recent company news and its implications\n\n"
|
||||||
|
"Provide specific, actionable insights with supporting evidence to help traders make "
|
||||||
|
"informed decisions. Make sure to append a Markdown table at the end of the report "
|
||||||
|
"to organise key points, making it easy to read."
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = ChatPromptTemplate.from_messages(
|
prompt = ChatPromptTemplate.from_messages(
|
||||||
|
|
@ -26,26 +61,27 @@ def create_social_media_analyst(llm):
|
||||||
(
|
(
|
||||||
"system",
|
"system",
|
||||||
"You are a helpful AI assistant, collaborating with other assistants."
|
"You are a helpful AI assistant, collaborating with other assistants."
|
||||||
" Use the provided tools to progress towards answering the question."
|
|
||||||
" If you are unable to fully answer, that's OK; another assistant with different tools"
|
" If you are unable to fully answer, that's OK; another assistant with different tools"
|
||||||
" will help where you left off. Execute what you can to make progress."
|
" will help where you left off. Execute what you can to make progress."
|
||||||
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
" If you or any other assistant has the FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** or deliverable,"
|
||||||
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
" prefix your response with FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL** so the team knows to stop."
|
||||||
" You have access to the following tools: {tool_names}.\n{system_message}"
|
"\n{system_message}"
|
||||||
"For your reference, the current date is {current_date}. {instrument_context}",
|
"For your reference, the current date is {current_date}. {instrument_context}\n\n"
|
||||||
|
"## Pre-loaded Context\n\n{prefetched_context}",
|
||||||
),
|
),
|
||||||
MessagesPlaceholder(variable_name="messages"),
|
MessagesPlaceholder(variable_name="messages"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = prompt.partial(system_message=system_message)
|
prompt = prompt.partial(system_message=system_message)
|
||||||
prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
|
|
||||||
prompt = prompt.partial(current_date=current_date)
|
prompt = prompt.partial(current_date=current_date)
|
||||||
prompt = prompt.partial(instrument_context=instrument_context)
|
prompt = prompt.partial(instrument_context=instrument_context)
|
||||||
|
prompt = prompt.partial(prefetched_context=prefetched_context)
|
||||||
|
|
||||||
chain = prompt | llm.bind_tools(tools)
|
# No tools remain — use direct invocation (no bind_tools, no tool loop)
|
||||||
|
chain = prompt | llm
|
||||||
|
|
||||||
result = run_tool_loop(chain, state["messages"], tools)
|
result = chain.invoke(state["messages"])
|
||||||
|
|
||||||
report = result.content or ""
|
report = result.content or ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,14 @@ def create_pm_decision_agent(llm):
|
||||||
"produce a structured JSON investment decision. "
|
"produce a structured JSON investment decision. "
|
||||||
"Consider: reducing risk where metrics are poor, acting on SELL recommendations, "
|
"Consider: reducing risk where metrics are poor, acting on SELL recommendations, "
|
||||||
"and adding positions in high-conviction candidates that pass constraints. "
|
"and adding positions in high-conviction candidates that pass constraints. "
|
||||||
|
"For every BUY you MUST set a stop_loss price (maximum acceptable loss level, "
|
||||||
|
"typically 5-15% below entry) and a take_profit price (expected sell target, "
|
||||||
|
"typically 10-30% above entry based on your thesis). "
|
||||||
"Output ONLY valid JSON matching this exact schema:\n"
|
"Output ONLY valid JSON matching this exact schema:\n"
|
||||||
"{\n"
|
"{\n"
|
||||||
' "sells": [{"ticker": "...", "shares": 0.0, "rationale": "..."}],\n'
|
' "sells": [{"ticker": "...", "shares": 0.0, "rationale": "..."}],\n'
|
||||||
' "buys": [{"ticker": "...", "shares": 0.0, "price_target": 0.0, '
|
' "buys": [{"ticker": "...", "shares": 0.0, "price_target": 0.0, '
|
||||||
|
'"stop_loss": 0.0, "take_profit": 0.0, '
|
||||||
'"sector": "...", "rationale": "...", "thesis": "..."}],\n'
|
'"sector": "...", "rationale": "...", "thesis": "..."}],\n'
|
||||||
' "holds": [{"ticker": "...", "rationale": "..."}],\n'
|
' "holds": [{"ticker": "...", "rationale": "..."}],\n'
|
||||||
' "cash_reserve_pct": 0.10,\n'
|
' "cash_reserve_pct": 0.10,\n'
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,57 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
from langchain_core.messages import HumanMessage, RemoveMessage
|
from langchain_core.messages import HumanMessage, RemoveMessage
|
||||||
|
|
||||||
|
|
||||||
|
def prefetch_tools_parallel(tool_calls: list[dict]) -> dict[str, str]:
|
||||||
|
"""Pre-fetch multiple LangChain tools in parallel using ThreadPoolExecutor.
|
||||||
|
|
||||||
|
Each entry in *tool_calls* must be a dict with:
|
||||||
|
- ``"tool"``: the LangChain tool object (must have an ``.invoke()`` method)
|
||||||
|
- ``"args"``: dict of keyword arguments to pass to ``tool.invoke()``
|
||||||
|
- ``"label"``: human-readable section header for the injected context block
|
||||||
|
|
||||||
|
Per-tool exceptions are caught so that a failure in one pre-fetch never
|
||||||
|
crashes the analyst node. The failing entry is replaced with an error
|
||||||
|
placeholder so the LLM can fall back to calling that tool itself.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict mapping ``label`` → result string (or error placeholder)
|
||||||
|
"""
|
||||||
|
results: dict[str, str] = {}
|
||||||
|
|
||||||
|
def _fetch_one(tc: dict) -> tuple[str, str]:
|
||||||
|
label: str = tc["label"]
|
||||||
|
try:
|
||||||
|
result = tc["tool"].invoke(tc["args"])
|
||||||
|
return label, str(result)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return label, f"[Error fetching {label}: {exc}]"
|
||||||
|
|
||||||
|
with ThreadPoolExecutor() as executor:
|
||||||
|
futures = {executor.submit(_fetch_one, tc): tc["label"] for tc in tool_calls}
|
||||||
|
for future in as_completed(futures):
|
||||||
|
label, result = future.result()
|
||||||
|
results[label] = result
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def format_prefetched_context(results: dict[str, str]) -> str:
|
||||||
|
"""Format a prefetched-results dict into a clean Markdown block.
|
||||||
|
|
||||||
|
Each key becomes a ``## <label>`` section header, with the corresponding
|
||||||
|
content beneath it. Sections are separated by a horizontal rule so the
|
||||||
|
LLM can locate each dataset quickly.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A single multi-line string ready for injection into a system prompt.
|
||||||
|
"""
|
||||||
|
sections = [f"## {label}\n\n{content}" for label, content in results.items()]
|
||||||
|
return "\n\n---\n\n".join(sections)
|
||||||
|
|
||||||
|
|
||||||
def build_instrument_context(ticker: str) -> str:
|
def build_instrument_context(ticker: str) -> str:
|
||||||
"""Describe the exact instrument so agents preserve exchange-qualified tickers."""
|
"""Describe the exact instrument so agents preserve exchange-qualified tickers."""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- =============================================================================
|
||||||
|
-- Portfolio Manager Agent — Migration 002
|
||||||
|
-- Migration: 002_add_trade_risk_levels.sql
|
||||||
|
-- Description: Adds stop_loss and take_profit columns to the trades table so
|
||||||
|
-- that the PM agent can record risk-management price levels for
|
||||||
|
-- every BUY trade.
|
||||||
|
-- Safe to re-run: uses ADD COLUMN IF NOT EXISTS.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE trades
|
||||||
|
ADD COLUMN IF NOT EXISTS stop_loss NUMERIC(18,4) CHECK (stop_loss IS NULL OR stop_loss > 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS take_profit NUMERIC(18,4) CHECK (take_profit IS NULL OR take_profit > 0);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN trades.stop_loss IS
|
||||||
|
'Price level at which the position should be exited to limit downside loss.';
|
||||||
|
COMMENT ON COLUMN trades.take_profit IS
|
||||||
|
'Price target at which the position should be sold to realise the expected profit.';
|
||||||
|
|
@ -219,6 +219,8 @@ class Trade:
|
||||||
trade_date: str = ""
|
trade_date: str = ""
|
||||||
rationale: str | None = None
|
rationale: str | None = None
|
||||||
signal_source: str | None = None # "scanner" | "holding_review" | "pm_agent"
|
signal_source: str | None = None # "scanner" | "holding_review" | "pm_agent"
|
||||||
|
stop_loss: float | None = None # Price level at which the position should be exited to limit loss
|
||||||
|
take_profit: float | None = None # Price target at which the position should be sold for profit
|
||||||
metadata: dict[str, Any] = field(default_factory=dict)
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
|
@ -234,12 +236,16 @@ class Trade:
|
||||||
"trade_date": self.trade_date,
|
"trade_date": self.trade_date,
|
||||||
"rationale": self.rationale,
|
"rationale": self.rationale,
|
||||||
"signal_source": self.signal_source,
|
"signal_source": self.signal_source,
|
||||||
|
"stop_loss": self.stop_loss,
|
||||||
|
"take_profit": self.take_profit,
|
||||||
"metadata": self.metadata,
|
"metadata": self.metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> "Trade":
|
def from_dict(cls, data: dict[str, Any]) -> "Trade":
|
||||||
"""Deserialise from a DB row or JSON dict."""
|
"""Deserialise from a DB row or JSON dict."""
|
||||||
|
raw_sl = data.get("stop_loss")
|
||||||
|
raw_tp = data.get("take_profit")
|
||||||
return cls(
|
return cls(
|
||||||
trade_id=data["trade_id"],
|
trade_id=data["trade_id"],
|
||||||
portfolio_id=data["portfolio_id"],
|
portfolio_id=data["portfolio_id"],
|
||||||
|
|
@ -251,6 +257,8 @@ class Trade:
|
||||||
trade_date=data.get("trade_date", ""),
|
trade_date=data.get("trade_date", ""),
|
||||||
rationale=data.get("rationale"),
|
rationale=data.get("rationale"),
|
||||||
signal_source=data.get("signal_source"),
|
signal_source=data.get("signal_source"),
|
||||||
|
stop_loss=float(raw_sl) if raw_sl is not None else None,
|
||||||
|
take_profit=float(raw_tp) if raw_tp is not None else None,
|
||||||
metadata=data.get("metadata") or {},
|
metadata=data.get("metadata") or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,8 @@ class PortfolioRepository:
|
||||||
price: float,
|
price: float,
|
||||||
sector: str | None = None,
|
sector: str | None = None,
|
||||||
industry: str | None = None,
|
industry: str | None = None,
|
||||||
|
stop_loss: float | None = None,
|
||||||
|
take_profit: float | None = None,
|
||||||
) -> Holding:
|
) -> Holding:
|
||||||
"""Buy shares and update portfolio cash and holdings."""
|
"""Buy shares and update portfolio cash and holdings."""
|
||||||
if shares <= 0:
|
if shares <= 0:
|
||||||
|
|
@ -173,6 +175,8 @@ class PortfolioRepository:
|
||||||
total_value=cost,
|
total_value=cost,
|
||||||
trade_date=datetime.now(timezone.utc).isoformat(),
|
trade_date=datetime.now(timezone.utc).isoformat(),
|
||||||
signal_source="pm_agent",
|
signal_source="pm_agent",
|
||||||
|
stop_loss=stop_loss,
|
||||||
|
take_profit=take_profit,
|
||||||
)
|
)
|
||||||
self._client.record_trade(trade)
|
self._client.record_trade(trade)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,10 @@ class TradeExecutor:
|
||||||
shares = float(buy.get("shares") or 0)
|
shares = float(buy.get("shares") or 0)
|
||||||
sector = buy.get("sector")
|
sector = buy.get("sector")
|
||||||
rationale = buy.get("rationale") or ""
|
rationale = buy.get("rationale") or ""
|
||||||
|
raw_sl = buy.get("stop_loss")
|
||||||
|
raw_tp = buy.get("take_profit")
|
||||||
|
stop_loss = float(raw_sl) if raw_sl is not None else None
|
||||||
|
take_profit = float(raw_tp) if raw_tp is not None else None
|
||||||
|
|
||||||
if not ticker or shares <= 0:
|
if not ticker or shares <= 0:
|
||||||
failed_trades.append({
|
failed_trades.append({
|
||||||
|
|
@ -196,6 +200,8 @@ class TradeExecutor:
|
||||||
shares,
|
shares,
|
||||||
price,
|
price,
|
||||||
sector=sector,
|
sector=sector,
|
||||||
|
stop_loss=stop_loss,
|
||||||
|
take_profit=take_profit,
|
||||||
)
|
)
|
||||||
executed_trades.append({
|
executed_trades.append({
|
||||||
"action": "BUY",
|
"action": "BUY",
|
||||||
|
|
@ -204,9 +210,16 @@ class TradeExecutor:
|
||||||
"price": price,
|
"price": price,
|
||||||
"sector": sector,
|
"sector": sector,
|
||||||
"rationale": rationale,
|
"rationale": rationale,
|
||||||
|
"stop_loss": stop_loss,
|
||||||
|
"take_profit": take_profit,
|
||||||
"trade_date": trade_date,
|
"trade_date": trade_date,
|
||||||
})
|
})
|
||||||
logger.info("BUY %s x %.2f @ %.2f", ticker, shares, price)
|
logger.info(
|
||||||
|
"BUY %s x %.2f @ %.2f (SL=%s TP=%s)",
|
||||||
|
ticker, shares, price,
|
||||||
|
f"{stop_loss:.2f}" if stop_loss is not None else "N/A",
|
||||||
|
f"{take_profit:.2f}" if take_profit is not None else "N/A",
|
||||||
|
)
|
||||||
except (InsufficientCashError, PortfolioError) as exc:
|
except (InsufficientCashError, PortfolioError) as exc:
|
||||||
failed_trades.append({
|
failed_trades.append({
|
||||||
"action": "BUY",
|
"action": "BUY",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue