diff options
Diffstat (limited to 'backend/internal/analyzer/openai')
-rw-r--r-- | backend/internal/analyzer/openai/openai.go | 126 | ||||
-rw-r--r-- | backend/internal/analyzer/openai/openai_test.go | 1 | ||||
-rw-r--r-- | backend/internal/analyzer/openai/options.go | 45 | ||||
-rw-r--r-- | backend/internal/analyzer/openai/system.txt | 34 |
4 files changed, 206 insertions, 0 deletions
diff --git a/backend/internal/analyzer/openai/openai.go b/backend/internal/analyzer/openai/openai.go new file mode 100644 index 0000000..0419c57 --- /dev/null +++ b/backend/internal/analyzer/openai/openai.go @@ -0,0 +1,126 @@ +package openai + +import ( + "context" + "encoding/json" + "fmt" + "math" + "strconv" + "strings" + "time" + + "github.com/ansg191/ibd-trader-backend/internal/analyzer" + "github.com/ansg191/ibd-trader-backend/internal/utils" + + "github.com/Rhymond/go-money" + "github.com/sashabaranov/go-openai" +) + +type Client interface { + CreateChatCompletion( + ctx context.Context, + request openai.ChatCompletionRequest, + ) (response openai.ChatCompletionResponse, err error) +} + +type Analyzer struct { + client Client + model string + systemMsg string + temperature float32 +} + +func NewAnalyzer(opts ...Option) *Analyzer { + a := &Analyzer{ + client: nil, + model: defaultModel, + systemMsg: defaultSystemMsg, + temperature: defaultTemperature, + } + for _, option := range opts { + option(a) + } + if a.client == nil { + panic("client is required") + } + + return a +} + +func (a *Analyzer) Analyze( + ctx context.Context, + symbol string, + price *money.Money, + rawAnalysis string, +) (*analyzer.Analysis, error) { + usrMsg := fmt.Sprintf( + "%s\n%s\n%s\n%s\n", + time.Now().Format(time.RFC3339), + symbol, + price.Display(), + rawAnalysis, + ) + res, err := a.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ + Model: a.model, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleSystem, + Content: a.systemMsg, + }, + { + Role: openai.ChatMessageRoleUser, + Content: usrMsg, + }, + }, + MaxTokens: 0, + Temperature: a.temperature, + ResponseFormat: &openai.ChatCompletionResponseFormat{Type: openai.ChatCompletionResponseFormatTypeJSONObject}, + }) + if err != nil { + return nil, err + } + + var resp response + if err = json.Unmarshal([]byte(res.Choices[0].Message.Content), &resp); err != nil { + return nil, fmt.Errorf("failed to unmarshal gpt response: %w", err) + } + + var action analyzer.ChartAction + switch strings.ToLower(resp.Action) { + case "buy": + action = analyzer.Buy + case "sell": + action = analyzer.Sell + case "hold": + action = analyzer.Hold + default: + action = analyzer.Unknown + } + + m, err := utils.ParseMoney(resp.Price) + if err != nil { + return nil, fmt.Errorf("failed to parse price: %w", err) + } + + confidence, err := strconv.ParseFloat(resp.Confidence, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse confidence: %w", err) + } + if confidence < 0 || confidence > 100 { + return nil, fmt.Errorf("confidence must be between 0 and 100, got %f", confidence) + } + + return &analyzer.Analysis{ + Action: action, + Price: m, + Reason: resp.Reason, + Confidence: uint8(math.Floor(confidence)), + }, nil +} + +type response struct { + Action string `json:"action"` + Price string `json:"price"` + Reason string `json:"reason"` + Confidence string `json:"confidence"` +} diff --git a/backend/internal/analyzer/openai/openai_test.go b/backend/internal/analyzer/openai/openai_test.go new file mode 100644 index 0000000..0aac709 --- /dev/null +++ b/backend/internal/analyzer/openai/openai_test.go @@ -0,0 +1 @@ +package openai diff --git a/backend/internal/analyzer/openai/options.go b/backend/internal/analyzer/openai/options.go new file mode 100644 index 0000000..11d691f --- /dev/null +++ b/backend/internal/analyzer/openai/options.go @@ -0,0 +1,45 @@ +package openai + +import ( + _ "embed" + + "github.com/sashabaranov/go-openai" +) + +//go:embed system.txt +var defaultSystemMsg string + +const defaultModel = openai.GPT4o +const defaultTemperature = 0.25 + +type Option func(*Analyzer) + +func WithClientConfig(cfg openai.ClientConfig) Option { + return func(a *Analyzer) { + a.client = openai.NewClientWithConfig(cfg) + } +} + +func WithDefaultConfig(apiKey string) Option { + return func(a *Analyzer) { + a.client = openai.NewClient(apiKey) + } +} + +func WithModel(model string) Option { + return func(a *Analyzer) { + a.model = model + } +} + +func WithSystemMsg(msg string) Option { + return func(a *Analyzer) { + a.systemMsg = msg + } +} + +func WithTemperature(temp float32) Option { + return func(a *Analyzer) { + a.temperature = temp + } +} diff --git a/backend/internal/analyzer/openai/system.txt b/backend/internal/analyzer/openai/system.txt new file mode 100644 index 0000000..82e3b2a --- /dev/null +++ b/backend/internal/analyzer/openai/system.txt @@ -0,0 +1,34 @@ +You're a stock analyzer. +You will be given a stock symbol, its current price, and its chart analysis. +Your job is to determine the best course of action to do with the stock (buy, sell, hold, or unknown), +the price at which the action should be taken, the reason for the action, and the confidence (0-100) +level you have in the action. + +The reason should be a paragraph explaining why you chose the action. + +The date the chart analysis was done may be mentioned in the chart analysis. +Make sure to take that into account when making your decision. +If the chart analysis is older than 1 week, lower your confidence accordingly and mention that in the reason. +If the chart analysis is too outdated, set the action to "unknown". + +The information will be given in the following format: +``` +<current datetime> +<stock symbol> +<current price> +<chart analysis> +``` + +Your response should be in the following JSON format: +``` +{ + "action": "<action>", + "price": "<price>", + "reason": "<reason>", + "confidence": "<confidence>" +} +``` +All fields are required and must be strings. +`action` must be one of the following: "buy", "sell", "hold", or "unknown". +`price` must contain the symbol for the currency and the price (e.g. "$100"). +The system WILL validate your response.
\ No newline at end of file |