Files
aitrade/pkg/app/strategy/normal.go
T
kaedwen ea141eb012
Build and Push Docker Image / build-and-push (push) Failing after 1m41s
initial
2026-07-02 20:09:44 +02:00

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
}