148 lines
4.0 KiB
Go
148 lines
4.0 KiB
Go
package strategy
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/pheinrich/aitrade/pkg/model"
|
|
)
|
|
|
|
type NormalStrategy struct {
|
|
stopLossEnabled bool
|
|
stopLossPercent float64
|
|
}
|
|
|
|
func NewNormalStrategy(stopLossEnabled bool, stopLossPercent float64) *NormalStrategy {
|
|
return &NormalStrategy{
|
|
stopLossEnabled: stopLossEnabled,
|
|
stopLossPercent: stopLossPercent,
|
|
}
|
|
}
|
|
|
|
func (s *NormalStrategy) Name() string {
|
|
return "normal"
|
|
}
|
|
|
|
func (s *NormalStrategy) GetRiskParams() RiskParameters {
|
|
return RiskParameters{
|
|
MaxParallelTrades: 5,
|
|
MaxTradesPerHour: 6,
|
|
PositionSizePercent: 4.0, // 3-5% of capital
|
|
StopLossPercent: 3.0,
|
|
}
|
|
}
|
|
|
|
func (s *NormalStrategy) Analyze(ctx context.Context, market *MarketData, news []*model.NewsArticle) (*TradeSignal, error) {
|
|
// Normal strategy: Trade on moderate positive sentiment for BUY
|
|
// Sell on negative sentiment or profit target
|
|
|
|
positiveCount := 0
|
|
negativeCount := 0
|
|
totalSentiment := 0.0
|
|
relevantArticles := 0
|
|
|
|
for _, article := range news {
|
|
if article.SentimentScore == nil {
|
|
continue
|
|
}
|
|
|
|
// Check if article mentions this symbol
|
|
if !strings.Contains(strings.ToUpper(article.Symbols), market.Symbol) {
|
|
continue
|
|
}
|
|
|
|
relevantArticles++
|
|
sentiment := *article.SentimentScore
|
|
totalSentiment += sentiment
|
|
|
|
if article.SentimentLabel == "positive" {
|
|
positiveCount++
|
|
} else if article.SentimentLabel == "negative" {
|
|
negativeCount++
|
|
}
|
|
}
|
|
|
|
// Need at least 2 relevant articles
|
|
if relevantArticles < 2 {
|
|
return nil, nil
|
|
}
|
|
|
|
avgSentiment := totalSentiment / float64(relevantArticles)
|
|
|
|
// SELL signal: Strong negative sentiment
|
|
if negativeCount > positiveCount && avgSentiment < -0.3 {
|
|
quantity := 0 // Will be filled from position
|
|
|
|
return &TradeSignal{
|
|
Symbol: market.Symbol,
|
|
Action: model.ActionSell,
|
|
Quantity: quantity,
|
|
Confidence: -avgSentiment * 0.9, // Convert negative to positive confidence
|
|
Reasoning: fmt.Sprintf("Normal SELL: %d negative vs %d positive articles, avg sentiment %.2f", negativeCount, positiveCount, avgSentiment),
|
|
}, nil
|
|
}
|
|
|
|
// BUY signal: Positive sentiment outweighs negative
|
|
if positiveCount <= negativeCount {
|
|
return nil, nil
|
|
}
|
|
|
|
// Moderate positive sentiment required (>0.3)
|
|
if avgSentiment < 0.3 {
|
|
return nil, nil
|
|
}
|
|
|
|
return &TradeSignal{
|
|
Symbol: market.Symbol,
|
|
Action: model.ActionBuy,
|
|
Quantity: 0, // Will be calculated by trader with current balance
|
|
Confidence: avgSentiment * 0.9,
|
|
Reasoning: fmt.Sprintf("Normal BUY: %d positive vs %d negative articles, avg sentiment %.2f", positiveCount, negativeCount, avgSentiment),
|
|
}, nil
|
|
}
|
|
|
|
// CalculatePositionSize determines how many shares to buy based on confidence and available capital
|
|
func (s *NormalStrategy) CalculatePositionSize(price float64, confidence float64, availableCapital float64) int {
|
|
// Base position size from strategy risk params
|
|
basePositionPercent := s.GetRiskParams().PositionSizePercent
|
|
|
|
// Scale position size by confidence (0.5 - 1.0 confidence → 0.5x - 1.0x of base)
|
|
// High confidence = larger position, low confidence = smaller position
|
|
confidenceMultiplier := 0.5 + (confidence * 0.5)
|
|
adjustedPercent := basePositionPercent * confidenceMultiplier
|
|
|
|
// Calculate position value and quantity
|
|
positionValue := availableCapital * (adjustedPercent / 100.0)
|
|
quantity := int(positionValue / price)
|
|
|
|
if quantity < 1 {
|
|
return 1 // Minimum 1 share
|
|
}
|
|
|
|
return quantity
|
|
}
|
|
|
|
// ValidateTradeValue checks if trade value is within absolute maximum
|
|
func ValidateTradeValue(quantity int, price float64, maxTradeValue float64) (int, error) {
|
|
if maxTradeValue <= 0 {
|
|
return quantity, nil // No limit
|
|
}
|
|
|
|
tradeValue := float64(quantity) * price
|
|
|
|
if tradeValue <= maxTradeValue {
|
|
return quantity, nil // Within limit
|
|
}
|
|
|
|
// Calculate max quantity that fits within limit
|
|
maxQuantity := int(maxTradeValue / price)
|
|
|
|
if maxQuantity < 1 {
|
|
return 0, fmt.Errorf("trade value would be $%.2f but max is $%.2f (price $%.2f too high for 1 share)",
|
|
tradeValue, maxTradeValue, price)
|
|
}
|
|
|
|
return maxQuantity, nil
|
|
}
|