diff --git a/backend/src/models/etrade.go b/backend/src/models/etrade.go index 57a18e7f..3a302f11 100644 --- a/backend/src/models/etrade.go +++ b/backend/src/models/etrade.go @@ -1,6 +1,8 @@ package models -import "backend/src/types" +import ( + "backend/src/types" +) type OAuthTokens struct { types.Model diff --git a/backend/src/services/etrade.go b/backend/src/services/etrade.go index 910dbd51..07f30cb6 100644 --- a/backend/src/services/etrade.go +++ b/backend/src/services/etrade.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "net/url" "time" "github.com/gomodule/oauth1/oauth" @@ -337,6 +338,77 @@ func (s *ETradeService) GetUserPortfolio(userID string) (models.UserPortfolio, e return portfolio, nil } +// PlaceOrder places an order on the E*Trade API +func (s *ETradeService) PlaceOrder(userID string, order types.Order) error { + oauthTokens, err := s.getLastOAuthTokens(userID) + if err != nil { + return fmt.Errorf("error getting oauth token: %s", err) + } + + client := newJSONClient() + + // Create the required order details structure + orderDetails := types.PreviewOrderRequest{ + ClientOrderID: fmt.Sprintf("%d", time.Now().UnixMilli()), // Use unique client order ID + Order: []types.OrderEntry{ + { + Instrument: []types.Instrument{ + { + Product: types.Product{ + SecurityType: order.SecurityType, // "EQ" for stocks, "OPTION" for options, etc. + Symbol: order.Symbol, + }, + OrderAction: order.Action, // "BUY", "SELL", etc. + QuantityType: types.QuantityType(order.QuantityType), // "QUANTITY", "DOLLARS", etc. + Quantity: order.Quantity, + }, + }, + OrderTerm: order.OrderTerm, // "GOOD_FOR_DAY", "IMMEDIATE_OR_CANCEL", etc. + MarketSession: order.MarketSession, // "REGULAR", "EXTENDED_HOURS" + PriceType: order.PriceType, // "MARKET", "LIMIT", etc. + StopPrice: order.StopPrice, // Used for stop orders + LimitPrice: order.LimitPrice, // Used for limit orders + AllOrNone: order.AllOrNone, // Boolean + }, + }, + } + + // Serialize orderDetails into JSON + orderData, err := json.Marshal(orderDetails) + if err != nil { + return fmt.Errorf("error serializing order details: %s", err) + } + + // Calculate the URL based on order preview vs. actual placement + baseURL := fmt.Sprintf("https://%s.etrade.com/v1/accounts/%s/orders/place", APIEnv, order.AccountID) + + // Make the API call using the OAuth credentials + orderValues, err := url.ParseQuery(string(orderData)) + if err != nil { + return fmt.Errorf("error parsing order data: %s", err) + } + + resp, err := oauthClient.Post(client, &oauth.Credentials{ + Token: oauthTokens.AccessToken, + Secret: oauthTokens.AccessSecret, + }, baseURL, orderValues) + if err != nil { + return fmt.Errorf("error sending order to E*Trade: %s", err) + } + defer resp.Body.Close() + + // Handle potential API errors + if resp.StatusCode >= 400 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("E*Trade API error: %s", body) + } + + // On success, you can extract the order ID from the response for confirmation + // or further tracking. + + return nil +} + // newJSONClient is a helper function that creates an HTTP client to interact with the E*Trade API // not using this causes the E*Trade API to return XML instead of JSON func newJSONClient() *http.Client { diff --git a/backend/src/types/model.go b/backend/src/types/model.go index 445bd0c2..a5dd46ce 100644 --- a/backend/src/types/model.go +++ b/backend/src/types/model.go @@ -118,3 +118,67 @@ type ClerkWebhookEvent struct { Object string `json:"object"` Type string `json:"type"` } + +// Represents a trade order +type Order struct { + AccountID string `json:"accountId"` + OrderType string `json:"orderType"` + Symbol string `json:"Product>symbol"` + SecurityType string `json:"Product>securityType"` // "EQ" for equity, "OPTION" + Action string `json:"orderAction"` // "BUY", "SELL" + QuantityType QuantityType `json:"quantityType"` // "QUANTITY", "DOLLARS" + Quantity int `json:"quantity"` + OrderTerm string `json:"orderTerm"` // "GOOD_FOR_DAY", "GOOD_UNTIL_CANCEL" + MarketSession string `json:"marketSession"` // "REGULAR", "EXTENDED_HOURS" + PriceType string `json:"priceType"` // "MARKET", "LIMIT" + LimitPrice float64 `json:"limitPrice"` + StopPrice float64 `json:"stopPrice"` + AllOrNone bool `json:"allOrNone"` + ClientOrderID string `json:"clientOrderId"` + IsPreview bool `json:"isPreview"` + OrderID string `json:"orderId,omitempty"` // Provided by the API after order placement + Status string `json:"status,omitempty"` // "OPEN", "FILLED", "CANCELLED" + PlacedTime time.Time `json:"placedTime,omitempty"` +} + +// QuantityType represents the type of quantity used in an order +type QuantityType string + +const ( + QuantityTypeQuantity QuantityType = "QUANTITY" + QuantityTypeDollars QuantityType = "DOLLARS" +) + +// PreviewOrderRequest represents the structure for previewing an order on E*Trade +type PreviewOrderRequest struct { + ClientOrderID string `json:"clientOrderId"` + Order []OrderEntry `json:"Order"` +} + +// OrderEntry represents a single order entry with the order details +type OrderEntry struct { + Instrument []Instrument `json:"Instrument"` + OrderAction string `json:"orderAction"` + QuantityType QuantityType `json:"quantityType"` + Quantity int `json:"quantity"` + OrderTerm string `json:"orderTerm"` + MarketSession string `json:"marketSession"` + PriceType string `json:"priceType"` + LimitPrice float64 `json:"limitPrice,omitempty"` + StopPrice float64 `json:"stopPrice,omitempty"` + AllOrNone bool `json:"allOrNone"` +} + +// Instrument represents a single financial instrument in an order +type Instrument struct { + Product Product `json:"Product"` + OrderAction string `json:"orderAction"` // Redundant, but sometimes expected by E*Trade APIs + QuantityType QuantityType `json:"quantityType"` // Also possibly redundant + Quantity int `json:"quantity"` +} + +// Product represents product details like security type and symbol +type Product struct { + SecurityType string `json:"securityType"` + Symbol string `json:"symbol"` +} diff --git a/frontend/services/etrade.ts b/frontend/services/etrade.ts index 5f8db0b6..e40b7df8 100644 --- a/frontend/services/etrade.ts +++ b/frontend/services/etrade.ts @@ -34,3 +34,19 @@ export const getPortoflio = async (id: string): Promise => { ); return response.data; } + +// export const copyPortfolio = async (id: string, portfolio: UserPortfolio): Promise => { +// const response: AxiosResponse = await axios.post( +// `http://${API_LINK}/etrade/portfolio/${id}`, +// portfolio, +// ); +// return response.status; +// } + +export const makeOrder = async (id: string, ticker: string, quantity: number, type: string): Promise => { + const response: AxiosResponse = await axios.post( + `http://${API_LINK}/etrade/order/${id}`, + { ticker: ticker, quantity: quantity, type: type }, + ); + return response.status; +} \ No newline at end of file