Commit ·
1e840aa
1
Parent(s): 9d444de
feat(prefs): risk tolerance + trading style now shape the signal
Browse files- app.py +12 -4
- crew/__init__.py +2 -1
- crew/config.py +51 -0
- crew/crew.py +50 -25
- crew/runner.py +4 -1
- crew/tasks.py +49 -8
app.py
CHANGED
|
@@ -196,6 +196,7 @@ def run_analysis(
|
|
| 196 |
from crew import (
|
| 197 |
LLMConfig,
|
| 198 |
OrchestratorConfig,
|
|
|
|
| 199 |
WatchlistRunner,
|
| 200 |
)
|
| 201 |
from crew.callbacks import ActivityEvent, ActivityFeedCallback, EventType
|
|
@@ -205,6 +206,12 @@ def run_analysis(
|
|
| 205 |
llm=LLMConfig(base_url=VLLM_ENDPOINT_URL),
|
| 206 |
)
|
| 207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
# Buffer events emitted by the runner/crew during ``_run_single``.
|
| 209 |
# The callback handler runs synchronously on the same thread, so
|
| 210 |
# a plain list plus a closure is sufficient — we drain it into
|
|
@@ -219,9 +226,7 @@ def run_analysis(
|
|
| 219 |
# Build the tools dict that maps each agent's name to its tool
|
| 220 |
# function list. Without this, every agent runs without tools
|
| 221 |
# and the LLM just hallucinates plausible-looking numbers
|
| 222 |
-
# instead of calling yfinance / ddgs / pandas-ta.
|
| 223 |
-
# cause of the "AAPL entry $192" hallucination from the first
|
| 224 |
-
# deploy — real AAPL was ~$293 at the time.)
|
| 225 |
from tools import (
|
| 226 |
search_news,
|
| 227 |
get_price_change,
|
|
@@ -243,7 +248,10 @@ def run_analysis(
|
|
| 243 |
}
|
| 244 |
|
| 245 |
runner = WatchlistRunner(
|
| 246 |
-
config=config,
|
|
|
|
|
|
|
|
|
|
| 247 |
)
|
| 248 |
|
| 249 |
for i, ticker in enumerate(tickers, 1):
|
|
|
|
| 196 |
from crew import (
|
| 197 |
LLMConfig,
|
| 198 |
OrchestratorConfig,
|
| 199 |
+
TradePreferences,
|
| 200 |
WatchlistRunner,
|
| 201 |
)
|
| 202 |
from crew.callbacks import ActivityEvent, ActivityFeedCallback, EventType
|
|
|
|
| 206 |
llm=LLMConfig(base_url=VLLM_ENDPOINT_URL),
|
| 207 |
)
|
| 208 |
|
| 209 |
+
preferences = TradePreferences(
|
| 210 |
+
risk_tolerance=risk_tolerance,
|
| 211 |
+
trading_style=trading_style,
|
| 212 |
+
portfolio_value=float(portfolio_value),
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
# Buffer events emitted by the runner/crew during ``_run_single``.
|
| 216 |
# The callback handler runs synchronously on the same thread, so
|
| 217 |
# a plain list plus a closure is sufficient — we drain it into
|
|
|
|
| 226 |
# Build the tools dict that maps each agent's name to its tool
|
| 227 |
# function list. Without this, every agent runs without tools
|
| 228 |
# and the LLM just hallucinates plausible-looking numbers
|
| 229 |
+
# instead of calling yfinance / ddgs / pandas-ta.
|
|
|
|
|
|
|
| 230 |
from tools import (
|
| 231 |
search_news,
|
| 232 |
get_price_change,
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
runner = WatchlistRunner(
|
| 251 |
+
config=config,
|
| 252 |
+
tools=crew_tools,
|
| 253 |
+
callback=callback,
|
| 254 |
+
preferences=preferences,
|
| 255 |
)
|
| 256 |
|
| 257 |
for i, ticker in enumerate(tickers, 1):
|
crew/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ tickers and produce structured trading signals (BUY/SELL/HOLD).
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
from crew.callbacks import ActivityEvent, ActivityFeedCallback, EventType
|
| 8 |
-
from crew.config import CrewConfig, LLMConfig, OrchestratorConfig
|
| 9 |
from crew.crew import CrewResult, FinAgentCrew
|
| 10 |
from crew.runner import WatchlistResult, WatchlistRunner
|
| 11 |
from crew.signals import Action, TradingSignal
|
|
@@ -20,6 +20,7 @@ __all__ = [
|
|
| 20 |
"FinAgentCrew",
|
| 21 |
"LLMConfig",
|
| 22 |
"OrchestratorConfig",
|
|
|
|
| 23 |
"TradingSignal",
|
| 24 |
"WatchlistResult",
|
| 25 |
"WatchlistRunner",
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
from crew.callbacks import ActivityEvent, ActivityFeedCallback, EventType
|
| 8 |
+
from crew.config import CrewConfig, LLMConfig, OrchestratorConfig, TradePreferences
|
| 9 |
from crew.crew import CrewResult, FinAgentCrew
|
| 10 |
from crew.runner import WatchlistResult, WatchlistRunner
|
| 11 |
from crew.signals import Action, TradingSignal
|
|
|
|
| 20 |
"FinAgentCrew",
|
| 21 |
"LLMConfig",
|
| 22 |
"OrchestratorConfig",
|
| 23 |
+
"TradePreferences",
|
| 24 |
"TradingSignal",
|
| 25 |
"WatchlistResult",
|
| 26 |
"WatchlistRunner",
|
crew/config.py
CHANGED
|
@@ -1,6 +1,57 @@
|
|
| 1 |
"""LLM configuration, timeouts, and constants for the orchestration layer."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass, field
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
@dataclass
|
|
|
|
| 1 |
"""LLM configuration, timeouts, and constants for the orchestration layer."""
|
| 2 |
|
| 3 |
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Literal
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
RiskTolerance = Literal["Conservative", "Moderate", "Aggressive"]
|
| 8 |
+
TradingStyle = Literal["Day Trading", "Swing Trading", "Position Trading"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass(frozen=True)
|
| 12 |
+
class TradePreferences:
|
| 13 |
+
"""User-selected risk / style / portfolio preferences.
|
| 14 |
+
|
| 15 |
+
These come from the Gradio UI dropdowns and are threaded end-to-end
|
| 16 |
+
into the Chief Strategist's task description so the final signal
|
| 17 |
+
reflects the user's stated profile. They also tune the re-ground
|
| 18 |
+
clamps in :class:`FinAgentCrew` — Conservative + Day Trading pushes
|
| 19 |
+
stop / target toward tight, short-horizon bands; Aggressive +
|
| 20 |
+
Position Trading opens them up.
|
| 21 |
+
|
| 22 |
+
Numbers here are percentages of entry (0.03 == 3 %).
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
risk_tolerance: RiskTolerance = "Moderate"
|
| 26 |
+
trading_style: TradingStyle = "Swing Trading"
|
| 27 |
+
portfolio_value: float = 10000.0
|
| 28 |
+
|
| 29 |
+
@property
|
| 30 |
+
def stop_pct(self) -> float:
|
| 31 |
+
return {
|
| 32 |
+
"Conservative": 0.015,
|
| 33 |
+
"Moderate": 0.03,
|
| 34 |
+
"Aggressive": 0.05,
|
| 35 |
+
}[self.risk_tolerance]
|
| 36 |
+
|
| 37 |
+
@property
|
| 38 |
+
def target_pct(self) -> float:
|
| 39 |
+
return {
|
| 40 |
+
"Day Trading": 0.02,
|
| 41 |
+
"Swing Trading": 0.05,
|
| 42 |
+
"Position Trading": 0.10,
|
| 43 |
+
}[self.trading_style]
|
| 44 |
+
|
| 45 |
+
@property
|
| 46 |
+
def stop_clamp(self) -> float:
|
| 47 |
+
"""Upper bound used by _reground_if_drifted. Slightly looser than
|
| 48 |
+
``stop_pct`` so the LLM has headroom, but tight enough to catch
|
| 49 |
+
the wide ATR-style stops Qwen sometimes emits."""
|
| 50 |
+
return self.stop_pct * 2.5
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
def target_clamp(self) -> float:
|
| 54 |
+
return self.target_pct * 3.0
|
| 55 |
|
| 56 |
|
| 57 |
@dataclass
|
crew/crew.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Optional
|
|
| 8 |
|
| 9 |
from crewai import Crew, Process
|
| 10 |
|
| 11 |
-
from crew.config import OrchestratorConfig
|
| 12 |
from crew.agents import (
|
| 13 |
create_llm,
|
| 14 |
create_market_scanner,
|
|
@@ -47,6 +47,7 @@ class FinAgentCrew:
|
|
| 47 |
config: OrchestratorConfig,
|
| 48 |
tools: dict[str, list],
|
| 49 |
callback: Optional[ActivityFeedCallback] = None,
|
|
|
|
| 50 |
):
|
| 51 |
"""Initialize the crew orchestrator.
|
| 52 |
|
|
@@ -60,10 +61,14 @@ class FinAgentCrew:
|
|
| 60 |
"risk_manager": [calculate_position_size, set_stop_loss],
|
| 61 |
}
|
| 62 |
callback: Optional activity feed callback for real-time UI updates
|
|
|
|
|
|
|
|
|
|
| 63 |
"""
|
| 64 |
self._config = config
|
| 65 |
self._tools = tools
|
| 66 |
self._callback = callback
|
|
|
|
| 67 |
self._parser = TradingSignalParser()
|
| 68 |
|
| 69 |
def run(self, ticker: str) -> CrewResult:
|
|
@@ -180,11 +185,14 @@ class FinAgentCrew:
|
|
| 180 |
market_task = create_market_scan_task(market_scanner, ticker)
|
| 181 |
fundamental_task = create_fundamental_task(fundamental_analyst, ticker)
|
| 182 |
technical_task = create_technical_task(technical_analyst, ticker)
|
| 183 |
-
risk_task = create_risk_task(
|
|
|
|
|
|
|
| 184 |
strategy_task = create_strategy_task(
|
| 185 |
chief_strategist,
|
| 186 |
ticker,
|
| 187 |
[market_task, fundamental_task, technical_task, risk_task],
|
|
|
|
| 188 |
)
|
| 189 |
|
| 190 |
return Crew(
|
|
@@ -324,20 +332,23 @@ class FinAgentCrew:
|
|
| 324 |
new_target = None if _bad(target) else round(target * scale, 2)
|
| 325 |
|
| 326 |
# Extra guard: after rescaling, stop / target should still be
|
| 327 |
-
# within a reasonable band of the new entry.
|
| 328 |
-
#
|
| 329 |
-
#
|
| 330 |
-
# something implausibly wide we let
|
| 331 |
-
#
|
|
|
|
|
|
|
|
|
|
| 332 |
def _stop_unreasonable(v: Optional[float]) -> bool:
|
| 333 |
if v is None:
|
| 334 |
return True
|
| 335 |
-
return abs(v - live_entry) / live_entry >
|
| 336 |
|
| 337 |
def _target_unreasonable(v: Optional[float]) -> bool:
|
| 338 |
if v is None:
|
| 339 |
return True
|
| 340 |
-
return abs(v - live_entry) / live_entry >
|
| 341 |
|
| 342 |
if _stop_unreasonable(new_stop):
|
| 343 |
new_stop = None
|
|
@@ -355,12 +366,14 @@ class FinAgentCrew:
|
|
| 355 |
)
|
| 356 |
return self._backfill_missing_prices(rescaled)
|
| 357 |
|
| 358 |
-
|
| 359 |
-
def _backfill_missing_prices(signal: TradingSignal) -> TradingSignal:
|
| 360 |
"""Back-fill stop-loss and target when the LLM emitted N/A or omitted them.
|
| 361 |
|
| 362 |
This ensures the UI card always shows three numeric prices rather
|
| 363 |
-
than a mix of numbers and N/A placeholders.
|
|
|
|
|
|
|
|
|
|
| 364 |
"""
|
| 365 |
entry = signal.entry_price
|
| 366 |
if entry is None or entry <= 0:
|
|
@@ -368,22 +381,29 @@ class FinAgentCrew:
|
|
| 368 |
|
| 369 |
stop = signal.stop_loss
|
| 370 |
target = signal.target_price
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
|
| 372 |
if stop is None or stop <= 0:
|
| 373 |
if signal.action == Action.BUY:
|
| 374 |
-
stop = round(entry *
|
| 375 |
elif signal.action == Action.SELL:
|
| 376 |
-
stop = round(entry * 1
|
| 377 |
else: # HOLD
|
| 378 |
-
stop = round(entry *
|
| 379 |
|
| 380 |
if target is None or target <= 0:
|
| 381 |
if signal.action == Action.BUY:
|
| 382 |
-
target = round(entry * 1
|
| 383 |
elif signal.action == Action.SELL:
|
| 384 |
-
target = round(entry *
|
| 385 |
else: # HOLD
|
| 386 |
-
target = round(entry * 1
|
| 387 |
|
| 388 |
return TradingSignal(
|
| 389 |
ticker=signal.ticker,
|
|
@@ -487,16 +507,21 @@ class FinAgentCrew:
|
|
| 487 |
else:
|
| 488 |
action = Action.HOLD
|
| 489 |
|
| 490 |
-
# Price bands
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
if action == Action.BUY:
|
| 492 |
-
stop = round(entry *
|
| 493 |
-
target = round(entry * 1
|
| 494 |
elif action == Action.SELL:
|
| 495 |
-
stop = round(entry * 1
|
| 496 |
-
target = round(entry *
|
| 497 |
else: # HOLD
|
| 498 |
-
stop = round(entry *
|
| 499 |
-
target = round(entry * 1
|
| 500 |
|
| 501 |
# Confidence = 50 baseline + up to 25 more for stronger moves.
|
| 502 |
confidence = int(
|
|
|
|
| 8 |
|
| 9 |
from crewai import Crew, Process
|
| 10 |
|
| 11 |
+
from crew.config import OrchestratorConfig, TradePreferences
|
| 12 |
from crew.agents import (
|
| 13 |
create_llm,
|
| 14 |
create_market_scanner,
|
|
|
|
| 47 |
config: OrchestratorConfig,
|
| 48 |
tools: dict[str, list],
|
| 49 |
callback: Optional[ActivityFeedCallback] = None,
|
| 50 |
+
preferences: Optional[TradePreferences] = None,
|
| 51 |
):
|
| 52 |
"""Initialize the crew orchestrator.
|
| 53 |
|
|
|
|
| 61 |
"risk_manager": [calculate_position_size, set_stop_loss],
|
| 62 |
}
|
| 63 |
callback: Optional activity feed callback for real-time UI updates
|
| 64 |
+
preferences: User trading preferences that shape the final
|
| 65 |
+
signal's stop / target bands. Defaults to Moderate /
|
| 66 |
+
Swing Trading / $10k when not supplied.
|
| 67 |
"""
|
| 68 |
self._config = config
|
| 69 |
self._tools = tools
|
| 70 |
self._callback = callback
|
| 71 |
+
self._preferences = preferences or TradePreferences()
|
| 72 |
self._parser = TradingSignalParser()
|
| 73 |
|
| 74 |
def run(self, ticker: str) -> CrewResult:
|
|
|
|
| 185 |
market_task = create_market_scan_task(market_scanner, ticker)
|
| 186 |
fundamental_task = create_fundamental_task(fundamental_analyst, ticker)
|
| 187 |
technical_task = create_technical_task(technical_analyst, ticker)
|
| 188 |
+
risk_task = create_risk_task(
|
| 189 |
+
risk_manager, ticker, [technical_task], self._preferences
|
| 190 |
+
)
|
| 191 |
strategy_task = create_strategy_task(
|
| 192 |
chief_strategist,
|
| 193 |
ticker,
|
| 194 |
[market_task, fundamental_task, technical_task, risk_task],
|
| 195 |
+
self._preferences,
|
| 196 |
)
|
| 197 |
|
| 198 |
return Crew(
|
|
|
|
| 332 |
new_target = None if _bad(target) else round(target * scale, 2)
|
| 333 |
|
| 334 |
# Extra guard: after rescaling, stop / target should still be
|
| 335 |
+
# within a reasonable band of the new entry. Bounds come from
|
| 336 |
+
# the user's preferences — Conservative / Day Trading locks
|
| 337 |
+
# everything down tight, Aggressive / Position Trading opens
|
| 338 |
+
# it up. If the LLM emitted something implausibly wide we let
|
| 339 |
+
# the back-fill replace it with the profile default band.
|
| 340 |
+
stop_clamp = self._preferences.stop_clamp
|
| 341 |
+
target_clamp = self._preferences.target_clamp
|
| 342 |
+
|
| 343 |
def _stop_unreasonable(v: Optional[float]) -> bool:
|
| 344 |
if v is None:
|
| 345 |
return True
|
| 346 |
+
return abs(v - live_entry) / live_entry > stop_clamp
|
| 347 |
|
| 348 |
def _target_unreasonable(v: Optional[float]) -> bool:
|
| 349 |
if v is None:
|
| 350 |
return True
|
| 351 |
+
return abs(v - live_entry) / live_entry > target_clamp
|
| 352 |
|
| 353 |
if _stop_unreasonable(new_stop):
|
| 354 |
new_stop = None
|
|
|
|
| 366 |
)
|
| 367 |
return self._backfill_missing_prices(rescaled)
|
| 368 |
|
| 369 |
+
def _backfill_missing_prices(self, signal: TradingSignal) -> TradingSignal:
|
|
|
|
| 370 |
"""Back-fill stop-loss and target when the LLM emitted N/A or omitted them.
|
| 371 |
|
| 372 |
This ensures the UI card always shows three numeric prices rather
|
| 373 |
+
than a mix of numbers and N/A placeholders. The band widths come
|
| 374 |
+
from the user's :class:`TradePreferences` so Conservative / Day
|
| 375 |
+
Trading produces tight short-horizon numbers and Aggressive /
|
| 376 |
+
Position Trading produces wider longer-horizon numbers.
|
| 377 |
"""
|
| 378 |
entry = signal.entry_price
|
| 379 |
if entry is None or entry <= 0:
|
|
|
|
| 381 |
|
| 382 |
stop = signal.stop_loss
|
| 383 |
target = signal.target_price
|
| 384 |
+
stop_pct = self._preferences.stop_pct
|
| 385 |
+
target_pct = self._preferences.target_pct
|
| 386 |
+
# HOLD cards use a half-width band so the card still renders
|
| 387 |
+
# with numeric stop / target without implying a directional
|
| 388 |
+
# trade the Strategist wasn't making.
|
| 389 |
+
hold_stop_pct = max(0.015, stop_pct * 0.5)
|
| 390 |
+
hold_target_pct = max(0.02, target_pct * 0.5)
|
| 391 |
|
| 392 |
if stop is None or stop <= 0:
|
| 393 |
if signal.action == Action.BUY:
|
| 394 |
+
stop = round(entry * (1 - stop_pct), 2)
|
| 395 |
elif signal.action == Action.SELL:
|
| 396 |
+
stop = round(entry * (1 + stop_pct), 2)
|
| 397 |
else: # HOLD
|
| 398 |
+
stop = round(entry * (1 - hold_stop_pct), 2)
|
| 399 |
|
| 400 |
if target is None or target <= 0:
|
| 401 |
if signal.action == Action.BUY:
|
| 402 |
+
target = round(entry * (1 + target_pct), 2)
|
| 403 |
elif signal.action == Action.SELL:
|
| 404 |
+
target = round(entry * (1 - target_pct), 2)
|
| 405 |
else: # HOLD
|
| 406 |
+
target = round(entry * (1 + hold_target_pct), 2)
|
| 407 |
|
| 408 |
return TradingSignal(
|
| 409 |
ticker=signal.ticker,
|
|
|
|
| 507 |
else:
|
| 508 |
action = Action.HOLD
|
| 509 |
|
| 510 |
+
# Price bands derived from user preferences.
|
| 511 |
+
stop_pct = self._preferences.stop_pct
|
| 512 |
+
target_pct = self._preferences.target_pct
|
| 513 |
+
hold_stop_pct = max(0.015, stop_pct * 0.5)
|
| 514 |
+
hold_target_pct = max(0.02, target_pct * 0.5)
|
| 515 |
+
|
| 516 |
if action == Action.BUY:
|
| 517 |
+
stop = round(entry * (1 - stop_pct), 2)
|
| 518 |
+
target = round(entry * (1 + target_pct), 2)
|
| 519 |
elif action == Action.SELL:
|
| 520 |
+
stop = round(entry * (1 + stop_pct), 2)
|
| 521 |
+
target = round(entry * (1 - target_pct), 2)
|
| 522 |
else: # HOLD
|
| 523 |
+
stop = round(entry * (1 - hold_stop_pct), 2)
|
| 524 |
+
target = round(entry * (1 + hold_target_pct), 2)
|
| 525 |
|
| 526 |
# Confidence = 50 baseline + up to 25 more for stronger moves.
|
| 527 |
confidence = int(
|
crew/runner.py
CHANGED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
| 5 |
from dataclasses import dataclass, field
|
| 6 |
from typing import Optional
|
| 7 |
|
| 8 |
-
from crew.config import OrchestratorConfig
|
| 9 |
from crew.crew import CrewResult, FinAgentCrew
|
| 10 |
from crew.callbacks import ActivityFeedCallback
|
| 11 |
|
|
@@ -28,10 +28,12 @@ class WatchlistRunner:
|
|
| 28 |
config: OrchestratorConfig,
|
| 29 |
tools: dict[str, list],
|
| 30 |
callback: Optional[ActivityFeedCallback] = None,
|
|
|
|
| 31 |
):
|
| 32 |
self._config = config
|
| 33 |
self._tools = tools
|
| 34 |
self._callback = callback
|
|
|
|
| 35 |
|
| 36 |
def run(self, watchlist: str) -> WatchlistResult:
|
| 37 |
"""Parse the watchlist and run the analysis pipeline for each ticker.
|
|
@@ -100,6 +102,7 @@ class WatchlistRunner:
|
|
| 100 |
config=self._config,
|
| 101 |
tools=self._tools,
|
| 102 |
callback=self._callback,
|
|
|
|
| 103 |
)
|
| 104 |
return crew.run(ticker)
|
| 105 |
except Exception as e:
|
|
|
|
| 5 |
from dataclasses import dataclass, field
|
| 6 |
from typing import Optional
|
| 7 |
|
| 8 |
+
from crew.config import OrchestratorConfig, TradePreferences
|
| 9 |
from crew.crew import CrewResult, FinAgentCrew
|
| 10 |
from crew.callbacks import ActivityFeedCallback
|
| 11 |
|
|
|
|
| 28 |
config: OrchestratorConfig,
|
| 29 |
tools: dict[str, list],
|
| 30 |
callback: Optional[ActivityFeedCallback] = None,
|
| 31 |
+
preferences: Optional[TradePreferences] = None,
|
| 32 |
):
|
| 33 |
self._config = config
|
| 34 |
self._tools = tools
|
| 35 |
self._callback = callback
|
| 36 |
+
self._preferences = preferences or TradePreferences()
|
| 37 |
|
| 38 |
def run(self, watchlist: str) -> WatchlistResult:
|
| 39 |
"""Parse the watchlist and run the analysis pipeline for each ticker.
|
|
|
|
| 102 |
config=self._config,
|
| 103 |
tools=self._tools,
|
| 104 |
callback=self._callback,
|
| 105 |
+
preferences=self._preferences,
|
| 106 |
)
|
| 107 |
return crew.run(ticker)
|
| 108 |
except Exception as e:
|
crew/tasks.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
| 1 |
"""Task factory functions with dependencies for CrewAI Task instances."""
|
| 2 |
|
|
|
|
|
|
|
| 3 |
from crewai import Task, Agent
|
| 4 |
|
|
|
|
|
|
|
| 5 |
|
| 6 |
def create_market_scan_task(agent: Agent, ticker: str) -> Task:
|
| 7 |
"""Create the market scanning task."""
|
|
@@ -56,19 +60,36 @@ def create_technical_task(agent: Agent, ticker: str) -> Task:
|
|
| 56 |
)
|
| 57 |
|
| 58 |
|
| 59 |
-
def create_risk_task(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
"""Create the risk assessment task.
|
| 61 |
|
| 62 |
Args:
|
| 63 |
agent: Risk Manager agent
|
| 64 |
ticker: Stock symbol
|
| 65 |
context: [technical_task] — depends on Technical Analyst output
|
|
|
|
|
|
|
| 66 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
return Task(
|
| 68 |
description=(
|
| 69 |
f"Calculate position sizing and stop-loss levels for {ticker}. "
|
| 70 |
f"Use the entry price from the Technical Analyst's recommendation "
|
| 71 |
-
f"to determine optimal position size and ATR-based stop-loss."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
),
|
| 73 |
expected_output=(
|
| 74 |
f"Risk parameters for {ticker} including: "
|
|
@@ -80,20 +101,40 @@ def create_risk_task(agent: Agent, ticker: str, context: list) -> Task:
|
|
| 80 |
)
|
| 81 |
|
| 82 |
|
| 83 |
-
def create_strategy_task(
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
Args:
|
| 87 |
agent: Chief Strategist agent
|
| 88 |
ticker: Stock symbol
|
| 89 |
context: [market_task, fundamental_task, technical_task, risk_task]
|
|
|
|
|
|
|
|
|
|
| 90 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
return Task(
|
| 92 |
description=(
|
| 93 |
-
f"Synthesize all analysis for {ticker} into a final trading
|
| 94 |
-
f"
|
| 95 |
-
f"
|
| 96 |
-
f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
f"Keep the response concise — do not deliberate at length.\n\n"
|
| 98 |
f"Output EXACTLY this format on its own lines, with NO extra prose:\n"
|
| 99 |
f"{ticker} — BUY (Confidence: 75%)\n"
|
|
|
|
| 1 |
"""Task factory functions with dependencies for CrewAI Task instances."""
|
| 2 |
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
from crewai import Task, Agent
|
| 6 |
|
| 7 |
+
from crew.config import TradePreferences
|
| 8 |
+
|
| 9 |
|
| 10 |
def create_market_scan_task(agent: Agent, ticker: str) -> Task:
|
| 11 |
"""Create the market scanning task."""
|
|
|
|
| 60 |
)
|
| 61 |
|
| 62 |
|
| 63 |
+
def create_risk_task(
|
| 64 |
+
agent: Agent,
|
| 65 |
+
ticker: str,
|
| 66 |
+
context: list,
|
| 67 |
+
preferences: Optional[TradePreferences] = None,
|
| 68 |
+
) -> Task:
|
| 69 |
"""Create the risk assessment task.
|
| 70 |
|
| 71 |
Args:
|
| 72 |
agent: Risk Manager agent
|
| 73 |
ticker: Stock symbol
|
| 74 |
context: [technical_task] — depends on Technical Analyst output
|
| 75 |
+
preferences: User preferences so the risk assessment matches the
|
| 76 |
+
investor's profile (portfolio size, risk appetite).
|
| 77 |
"""
|
| 78 |
+
prefs = preferences or TradePreferences()
|
| 79 |
+
stop_target_pct = int(round(prefs.stop_pct * 100))
|
| 80 |
+
target_target_pct = int(round(prefs.target_pct * 100))
|
| 81 |
+
|
| 82 |
return Task(
|
| 83 |
description=(
|
| 84 |
f"Calculate position sizing and stop-loss levels for {ticker}. "
|
| 85 |
f"Use the entry price from the Technical Analyst's recommendation "
|
| 86 |
+
f"to determine optimal position size and ATR-based stop-loss.\n\n"
|
| 87 |
+
f"The user's profile:\n"
|
| 88 |
+
f"- Risk tolerance: {prefs.risk_tolerance} "
|
| 89 |
+
f"(target stop-loss distance ~{stop_target_pct}% from entry)\n"
|
| 90 |
+
f"- Trading style: {prefs.trading_style} "
|
| 91 |
+
f"(target profit distance ~{target_target_pct}% from entry)\n"
|
| 92 |
+
f"- Portfolio value: ${prefs.portfolio_value:,.0f}\n"
|
| 93 |
),
|
| 94 |
expected_output=(
|
| 95 |
f"Risk parameters for {ticker} including: "
|
|
|
|
| 101 |
)
|
| 102 |
|
| 103 |
|
| 104 |
+
def create_strategy_task(
|
| 105 |
+
agent: Agent,
|
| 106 |
+
ticker: str,
|
| 107 |
+
context: list,
|
| 108 |
+
preferences: Optional[TradePreferences] = None,
|
| 109 |
+
) -> Task:
|
| 110 |
+
r"""Create the strategy synthesis task.
|
| 111 |
|
| 112 |
Args:
|
| 113 |
agent: Chief Strategist agent
|
| 114 |
ticker: Stock symbol
|
| 115 |
context: [market_task, fundamental_task, technical_task, risk_task]
|
| 116 |
+
preferences: User preferences that shape the final signal's
|
| 117 |
+
stop / target distances and horizon. When ``None``, falls
|
| 118 |
+
back to a Moderate / Swing Trading / \$10k profile.
|
| 119 |
"""
|
| 120 |
+
prefs = preferences or TradePreferences()
|
| 121 |
+
stop_pct = int(round(prefs.stop_pct * 100))
|
| 122 |
+
target_pct = int(round(prefs.target_pct * 100))
|
| 123 |
+
|
| 124 |
return Task(
|
| 125 |
description=(
|
| 126 |
+
f"Synthesize all analysis for {ticker} into a final trading "
|
| 127 |
+
f"signal for a **{prefs.risk_tolerance} "
|
| 128 |
+
f"{prefs.trading_style}** investor "
|
| 129 |
+
f"with a ${prefs.portfolio_value:,.0f} portfolio.\n\n"
|
| 130 |
+
f"Use the current price reported by get_price_change as Entry.\n"
|
| 131 |
+
f"Profile-specific stop / target distances:\n"
|
| 132 |
+
f"- Stop Loss: approximately {stop_pct}% from Entry "
|
| 133 |
+
f"(tighter for Conservative, wider for Aggressive)\n"
|
| 134 |
+
f"- Target: approximately {target_pct}% from Entry "
|
| 135 |
+
f"(smaller for Day Trading, larger for Position Trading)\n\n"
|
| 136 |
+
f"For BUY: Stop Loss below Entry, Target above Entry.\n"
|
| 137 |
+
f"For SELL: Stop Loss above Entry, Target below Entry.\n"
|
| 138 |
f"Keep the response concise — do not deliberate at length.\n\n"
|
| 139 |
f"Output EXACTLY this format on its own lines, with NO extra prose:\n"
|
| 140 |
f"{ticker} — BUY (Confidence: 75%)\n"
|