Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add stop-loss and take-profit functionality #10

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,20 @@ You can also specify a `--show-reasoning` flag to print the reasoning of each ag
```bash
poetry run python src/agents.py --ticker AAPL --show-reasoning
```

You can optionally specify the start and end dates to make decisions for a specific time period.

```bash
poetry run python src/agents.py --ticker AAPL --start-date 2024-01-01 --end-date 2024-03-01
```

Risk management parameters can be configured using:
```bash
poetry run python src/agents.py --ticker AAPL --stop-loss 0.05 --take-profit 0.15
```
- `--stop-loss`: Stop loss percentage (default: 5%)
- `--take-profit`: Take profit percentage (default: 15%)

### Running the Backtester

```bash
Expand Down
102 changes: 87 additions & 15 deletions src/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ def risk_management_agent(state: AgentState):
data = state["data"]

prices_df = prices_to_df(data["prices"])
current_price = prices_df['close'].iloc[-1]

# Fetch messages from other agents
quant_message = next(msg for msg in state["messages"] if msg.name == "quant_agent")
Expand All @@ -417,7 +418,6 @@ def risk_management_agent(state: AgentState):
technical_signals = ast.literal_eval(quant_message.content)
sentiment_signals = ast.literal_eval(sentiment_message.content)


print(f"fundamental_signals: {fundamental_signals}")
print(f"technical_signals: {technical_signals}")
print(f"sentiment_signals: {sentiment_signals}")
Expand Down Expand Up @@ -459,7 +459,7 @@ def risk_management_agent(state: AgentState):

# 3. Position Size Limits
# Consider total portfolio value, not just cash
current_stock_value = portfolio['stock'] * prices_df['close'].iloc[-1]
current_stock_value = portfolio['stock'] * current_price
total_portfolio_value = portfolio['cash'] + current_stock_value

base_position_size = total_portfolio_value * 0.25 # Start with 25% max position of total portfolio
Expand All @@ -474,7 +474,30 @@ def risk_management_agent(state: AgentState):
# Keep base size for low risk
max_position_size = base_position_size

# 4. Stress Testing
# 4. Stop-Loss and Take-Profit Check
stop_loss_triggered = False
take_profit_triggered = False
price_action_signal = "hold"
stop_loss_price = None
take_profit_price = None

if portfolio['stock'] > 0 and portfolio['entry_price'] is not None:
# Calculate current return
current_return = (current_price - portfolio['entry_price']) / portfolio['entry_price']

# Check stop-loss
if current_return <= -portfolio['stop_loss']:
stop_loss_triggered = True
price_action_signal = "sell"
stop_loss_price = portfolio['entry_price'] * (1 - portfolio['stop_loss'])

# Check take-profit
elif current_return >= portfolio['take_profit']:
take_profit_triggered = True
price_action_signal = "sell"
take_profit_price = portfolio['entry_price'] * (1 + portfolio['take_profit'])

# 5. Stress Testing
stress_test_scenarios = {
"market_crash": -0.20,
"moderate_decline": -0.10,
Expand All @@ -492,7 +515,7 @@ def risk_management_agent(state: AgentState):
"portfolio_impact": portfolio_impact
}

# 5. Risk-Adjusted Signals Analysis
# 6. Risk-Adjusted Signals Analysis
# Convert all confidences to numeric for proper comparison
def parse_confidence(conf_str):
return float(conf_str.replace('%', '')) / 100.0
Expand All @@ -511,10 +534,12 @@ def parse_confidence(conf_str):
# Cap risk score at 10
risk_score = min(round(risk_score), 10)

# 6. Generate Trading Action
# 7. Generate Trading Action
# If risk is very high, hold. If moderately high, consider reducing.
# Else, follow fundamental signal as a baseline.
if risk_score >= 8:
if stop_loss_triggered or take_profit_triggered:
trading_action = price_action_signal
elif risk_score >= 8:
trading_action = "hold"
elif risk_score >= 6:
trading_action = "reduce"
Expand All @@ -530,11 +555,20 @@ def parse_confidence(conf_str):
"value_at_risk_95": float(var_95),
"max_drawdown": float(max_drawdown),
"market_risk_score": market_risk_score,
"stress_test_results": stress_test_results
"stress_test_results": stress_test_results,
"stop_loss_triggered": stop_loss_triggered,
"take_profit_triggered": take_profit_triggered,
"stop_loss_price": stop_loss_price,
"take_profit_price": take_profit_price,
"current_price": current_price
},
"reasoning": f"Risk Score {risk_score}/10: Market Risk={market_risk_score}, "
f"Volatility={volatility:.2%}, VaR={var_95:.2%}, "
f"Max Drawdown={max_drawdown:.2%}"
"reasoning": (
f"Risk Score {risk_score}/10: Market Risk={market_risk_score}, "
f"Volatility={volatility:.2%}, VaR={var_95:.2%}, "
f"Max Drawdown={max_drawdown:.2%}"
+ (f", Stop-Loss Triggered at {stop_loss_price:.2f}" if stop_loss_triggered else "")
+ (f", Take-Profit Triggered at {take_profit_price:.2f}" if take_profit_triggered else "")
)
}

# Create the risk management message
Expand All @@ -554,6 +588,11 @@ def portfolio_management_agent(state: AgentState):
"""Makes final trading decisions and generates orders"""
show_reasoning = state["metadata"]["show_reasoning"]
portfolio = state["data"]["portfolio"]
data = state["data"]

# Get current price
prices_df = prices_to_df(data["prices"])
current_price = prices_df['close'].iloc[-1]

# Get the quant agent, fundamentals agent, and risk management agent messages
quant_message = next(msg for msg in state["messages"] if msg.name == "quant_agent")
Expand All @@ -574,6 +613,7 @@ def portfolio_management_agent(state: AgentState):
- You MUST NOT exceed the max_position_size specified by the risk manager
- You MUST follow the trading_action (buy/sell/hold) recommended by risk management
- These are hard constraints that cannot be overridden by other signals
- If stop-loss or take-profit is triggered, you MUST execute the sell order

When weighing the different signals for direction and timing:
1. Fundamental Analysis (50% weight)
Expand All @@ -589,7 +629,7 @@ def portfolio_management_agent(state: AgentState):
- Can influence sizing within risk limits

The decision process should be:
1. First check risk management constraints
1. First check risk management constraints and stop-loss/take-profit
2. Then evaluate fundamental outlook
3. Use technical analysis for timing
4. Consider sentiment for final adjustment
Expand All @@ -606,7 +646,8 @@ def portfolio_management_agent(state: AgentState):
- Only buy if you have available cash
- Only sell if you have shares to sell
- Quantity must be ≤ current position for sells
- Quantity must be ≤ max_position_size from risk management"""
- Quantity must be ≤ max_position_size from risk management
- If stop-loss or take-profit triggered, sell entire position"""
),
(
"human",
Expand All @@ -621,6 +662,8 @@ def portfolio_management_agent(state: AgentState):
Portfolio:
Cash: {portfolio_cash}
Current Position: {portfolio_stock} shares
Current Price: {current_price}
Entry Price: {entry_price}

Only include the action, quantity, reasoning, confidence, and agent_signals in your output as JSON. Do not include any JSON markdown.

Expand All @@ -640,15 +683,31 @@ def portfolio_management_agent(state: AgentState):
"sentiment_message": sentiment_message.content,
"risk_message": risk_message.content,
"portfolio_cash": f"{portfolio['cash']:.2f}",
"portfolio_stock": portfolio["stock"]
"portfolio_stock": portfolio["stock"],
"current_price": f"{current_price:.2f}",
"entry_price": f"{portfolio['entry_price']:.2f}" if portfolio['entry_price'] is not None else "None"
}
)

# Invoke the LLM
result = llm.invoke(prompt)

# Parse the decision
try:
decision = json.loads(result.content)
# Update entry price if buying into a new position
if decision["action"] == "buy" and portfolio["stock"] == 0:
portfolio["entry_price"] = current_price
# Reset entry price if selling entire position
elif decision["action"] == "sell" and decision["quantity"] >= portfolio["stock"]:
portfolio["entry_price"] = None
except Exception as e:
print(f"Error parsing decision: {e}")
decision = {"action": "hold", "quantity": 0, "confidence": 0.0, "reasoning": "Error parsing decision"}

# Create the portfolio management message
message = HumanMessage(
content=result.content,
content=json.dumps(decision),
name="portfolio_management",
)

Expand Down Expand Up @@ -726,6 +785,8 @@ def run_hedge_fund(ticker: str, start_date: str, end_date: str, portfolio: dict,
parser.add_argument('--start-date', type=str, help='Start date (YYYY-MM-DD). Defaults to 3 months before end date')
parser.add_argument('--end-date', type=str, help='End date (YYYY-MM-DD). Defaults to today')
parser.add_argument('--show-reasoning', action='store_true', help='Show reasoning from each agent')
parser.add_argument('--stop-loss', type=float, default=0.05, help='Stop loss percentage (default: 5%%)')
parser.add_argument('--take-profit', type=float, default=0.15, help='Take profit percentage (default: 15%%)')

args = parser.parse_args()

Expand All @@ -742,10 +803,21 @@ def run_hedge_fund(ticker: str, start_date: str, end_date: str, portfolio: dict,
except ValueError:
raise ValueError("End date must be in YYYY-MM-DD format")

# Validate stop-loss and take-profit
if not (0 < args.stop_loss < 1):
raise ValueError("Stop loss must be between 0 and 1 (e.g., 0.05 for 5%)")
if not (0 < args.take_profit < 1):
raise ValueError("Take profit must be between 0 and 1 (e.g., 0.15 for 15%)")
if args.take_profit <= args.stop_loss:
raise ValueError("Take profit must be greater than stop loss")

# Sample portfolio - you might want to make this configurable too
portfolio = {
"cash": 100000.0, # $100,000 initial cash
"stock": 0 # No initial stock position
"stock": 0, # No initial stock position
"stop_loss": args.stop_loss,
"take_profit": args.take_profit,
"entry_price": None # Will be set when a position is opened
}

result = run_hedge_fund(
Expand Down