aboutsummaryrefslogtreecommitdiff
path: root/backend/internal/analyzer/openai
diff options
context:
space:
mode:
authorGravatar Anshul Gupta <ansg191@anshulg.com> 2024-08-11 13:15:50 -0700
committerGravatar Anshul Gupta <ansg191@anshulg.com> 2024-08-11 13:15:50 -0700
commit6a3c21fb0b1c126849f2bbff494403bbe901448e (patch)
tree5d7805524357c2c8a9819c39d2051a4e3633a1d5 /backend/internal/analyzer/openai
parent29c6040a51616e9e4cf6c70ee16391b2a3b238c9 (diff)
parentf34b92ded11b07f78575ac62c260a380c468e5ea (diff)
downloadibd-trader-6a3c21fb0b1c126849f2bbff494403bbe901448e.tar.gz
ibd-trader-6a3c21fb0b1c126849f2bbff494403bbe901448e.tar.zst
ibd-trader-6a3c21fb0b1c126849f2bbff494403bbe901448e.zip
Merge remote-tracking branch 'backend/main'
Diffstat (limited to 'backend/internal/analyzer/openai')
-rw-r--r--backend/internal/analyzer/openai/openai.go126
-rw-r--r--backend/internal/analyzer/openai/openai_test.go1
-rw-r--r--backend/internal/analyzer/openai/options.go45
-rw-r--r--backend/internal/analyzer/openai/system.txt34
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