Merge pull request #98 from aguzererler/copilot/add-trades-with-stop-loss
Add stop_loss and take_profit to BUY trade records
This commit is contained in:
commit
73cb317120
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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