mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-15 16:33:50 +00:00
Added Oanda module
This commit is contained in:
parent
494df7827b
commit
2d4c5df187
@ -21,6 +21,8 @@ var (
|
|||||||
ErrPositionClosed = errors.New("position closed")
|
ErrPositionClosed = errors.New("position closed")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Broker = (*TestBroker)(nil) // Compile-time interface check.
|
||||||
|
|
||||||
func Backtest(trader *Trader) {
|
func Backtest(trader *Trader) {
|
||||||
switch broker := trader.Broker.(type) {
|
switch broker := trader.Broker.(type) {
|
||||||
case *TestBroker:
|
case *TestBroker:
|
||||||
|
@ -58,8 +58,8 @@ type Position interface {
|
|||||||
type Broker interface {
|
type Broker interface {
|
||||||
Signaler
|
Signaler
|
||||||
// Candles returns a dataframe of candles for the given symbol, frequency, and count by querying the broker.
|
// Candles returns a dataframe of candles for the given symbol, frequency, and count by querying the broker.
|
||||||
Candles(symbol string, frequency string, count int) (*DataFrame, error)
|
Candles(symbol, frequency string, count int) (*DataFrame, error)
|
||||||
MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error)
|
MarketOrder(symbol string, units, stopLoss, takeProfit float64) (Order, error)
|
||||||
NAV() float64 // NAV returns the net asset value of the account.
|
NAV() float64 // NAV returns the net asset value of the account.
|
||||||
PL() float64 // PL returns the profit or loss of the account.
|
PL() float64 // PL returns the profit or loss of the account.
|
||||||
OpenOrders() []Order
|
OpenOrders() []Order
|
||||||
|
38
data.go
38
data.go
@ -78,7 +78,7 @@ type Frame interface {
|
|||||||
Lows() Series
|
Lows() Series
|
||||||
Closes() Series
|
Closes() Series
|
||||||
Volumes() Series
|
Volumes() Series
|
||||||
PushCandle(date time.Time, open, high, low, close, volume float64) error
|
PushCandle(date time.Time, open, high, low, close float64, volume int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppliedSeries is like Series, but it applies a function to each row of data before returning it.
|
// AppliedSeries is like Series, but it applies a function to each row of data before returning it.
|
||||||
@ -439,6 +439,25 @@ type DataFrame struct {
|
|||||||
// data *df.DataFrame // DataFrame with a Date, Open, High, Low, Close, and Volume column.
|
// data *df.DataFrame // DataFrame with a Date, Open, High, Low, Close, and Volume column.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewDataFrame(series ...Series) *DataFrame {
|
||||||
|
d := &DataFrame{}
|
||||||
|
d.PushSeries(series...)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDOHLCVDataFrame returns a DataFrame with empty Date, Open, High, Low, Close, and Volume columns.
|
||||||
|
// Use the PushCandle method to add candlesticks in an easy and type-safe way.
|
||||||
|
func NewDOHLCVDataFrame() *DataFrame {
|
||||||
|
return NewDataFrame(
|
||||||
|
NewDataSeries(df.NewSeriesTime("Date", nil)),
|
||||||
|
NewDataSeries(df.NewSeriesFloat64("Open", nil)),
|
||||||
|
NewDataSeries(df.NewSeriesFloat64("High", nil)),
|
||||||
|
NewDataSeries(df.NewSeriesFloat64("Low", nil)),
|
||||||
|
NewDataSeries(df.NewSeriesFloat64("Close", nil)),
|
||||||
|
NewDataSeries(df.NewSeriesInt64("Volume", nil)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Copy copies the DataFrame from start to end (inclusive). If end is -1, it will copy to the end of the DataFrame. If start is out of bounds, nil is returned.
|
// Copy copies the DataFrame from start to end (inclusive). If end is -1, it will copy to the end of the DataFrame. If start is out of bounds, nil is returned.
|
||||||
func (d *DataFrame) Copy(start, end int) Frame {
|
func (d *DataFrame) Copy(start, end int) Frame {
|
||||||
out := &DataFrame{}
|
out := &DataFrame{}
|
||||||
@ -468,6 +487,9 @@ func (d *DataFrame) Len() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataFrame) String() string {
|
func (d *DataFrame) String() string {
|
||||||
|
if d == nil {
|
||||||
|
return fmt.Sprintf("%T[nil]", d)
|
||||||
|
}
|
||||||
names := d.Names() // Defines the order of the columns.
|
names := d.Names() // Defines the order of the columns.
|
||||||
series := make([]Series, len(names))
|
series := make([]Series, len(names))
|
||||||
for i, name := range names {
|
for i, name := range names {
|
||||||
@ -596,16 +618,16 @@ func (d *DataFrame) ContainsDOHLCV() bool {
|
|||||||
return d.Contains("Date", "Open", "High", "Low", "Close", "Volume")
|
return d.Contains("Date", "Open", "High", "Low", "Close", "Volume")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataFrame) PushCandle(date time.Time, open, high, low, close, volume float64) error {
|
func (d *DataFrame) PushCandle(date time.Time, open, high, low, close float64, volume int64) error {
|
||||||
if len(d.series) == 0 {
|
if len(d.series) == 0 {
|
||||||
d.PushSeries([]Series{
|
d.PushSeries(
|
||||||
NewDataSeries(df.NewSeriesTime("Date", nil, date)),
|
NewDataSeries(df.NewSeriesTime("Date", nil, date)),
|
||||||
NewDataSeries(df.NewSeriesFloat64("Open", nil, open)),
|
NewDataSeries(df.NewSeriesFloat64("Open", nil, open)),
|
||||||
NewDataSeries(df.NewSeriesFloat64("High", nil, high)),
|
NewDataSeries(df.NewSeriesFloat64("High", nil, high)),
|
||||||
NewDataSeries(df.NewSeriesFloat64("Low", nil, low)),
|
NewDataSeries(df.NewSeriesFloat64("Low", nil, low)),
|
||||||
NewDataSeries(df.NewSeriesFloat64("Close", nil, close)),
|
NewDataSeries(df.NewSeriesFloat64("Close", nil, close)),
|
||||||
NewDataSeries(df.NewSeriesFloat64("Volume", nil, volume)),
|
NewDataSeries(df.NewSeriesInt64("Volume", nil, volume)),
|
||||||
}...)
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !d.ContainsDOHLCV() {
|
if !d.ContainsDOHLCV() {
|
||||||
@ -773,12 +795,6 @@ func (d *DataFrame) Time(column string, i int) time.Time {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDataFrame(series ...Series) *DataFrame {
|
|
||||||
d := &DataFrame{}
|
|
||||||
d.PushSeries(series...)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
type DataCSVLayout struct {
|
type DataCSVLayout struct {
|
||||||
LatestFirst bool // Whether the latest data is first in the dataframe. If false, the latest data is last.
|
LatestFirst bool // Whether the latest data is first in the dataframe. If false, the latest data is last.
|
||||||
DateFormat string // The format of the date column. Example: "03/22/2006". See https://pkg.go.dev/time#pkg-constants for more information.
|
DateFormat string // The format of the date column. Example: "03/22/2006". See https://pkg.go.dev/time#pkg-constants for more information.
|
||||||
|
57
oanda/defs.go
Normal file
57
oanda/defs.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package oanda
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CandlestickResponse represents the response from the Oanda API for a request for candlestick data.
|
||||||
|
type CandlestickResponse struct {
|
||||||
|
Instrument string `json:"instrument"` // The instrument whose Prices are represented by the candlesticks.
|
||||||
|
Granularity string `json:"granularity"` // The granularity of the candlesticks provided.
|
||||||
|
Candles []Candlestick `json:"candles"` // The list of candlesticks that satisfy the request.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Candlestick represents a single candlestick.
|
||||||
|
type Candlestick struct {
|
||||||
|
Time time.Time `json:"time"` // The start time of the candlestick.
|
||||||
|
Bid *CandlestickData `json:"bid"` // The candlestick data based on bids. Only provided if bid-based candles were requested.
|
||||||
|
Ask *CandlestickData `json:"ask"` // The candlestick data based on asks. Only provided if ask-based candles were requested.
|
||||||
|
Mid *CandlestickData `json:"mid"` // The candlestick data based on midpoints. Only provided if midpoint-based candles were requested.
|
||||||
|
Volume int `json:"volume"` // The number of prices created during the time-range represented by the candlestick.
|
||||||
|
Complete bool `json:"complete"` // A flag indicating if the candlestick is complete. A complete candlestick is one whose ending time is not in the future.
|
||||||
|
}
|
||||||
|
|
||||||
|
// CandlestickData represents the price information for a candlestick.
|
||||||
|
type CandlestickData struct {
|
||||||
|
// The first (open) price in the time-range represented by the candlestick.
|
||||||
|
O string `json:"o"`
|
||||||
|
// The highest price in the time-range represented by the candlestick.
|
||||||
|
H string `json:"h"`
|
||||||
|
// The lowest price in the time-range represented by the candlestick.
|
||||||
|
L string `json:"l"`
|
||||||
|
// The last (closing) price in the time-range represented by the candlestick.
|
||||||
|
C string `json:"c"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d CandlestickData) Parse(o, h, l, c *float64) error {
|
||||||
|
var err error
|
||||||
|
*o, err = strconv.ParseFloat(d.O, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing O field of CandlestickData: %w", err)
|
||||||
|
}
|
||||||
|
*h, err = strconv.ParseFloat(d.H, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing H field of CandlestickData: %w", err)
|
||||||
|
}
|
||||||
|
*l, err = strconv.ParseFloat(d.L, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing L field of CandlestickData: %w", err)
|
||||||
|
}
|
||||||
|
*c, err = strconv.ParseFloat(d.C, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing C field of CandlestickData: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
1
oanda/endpoints.go
Normal file
1
oanda/endpoints.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package oanda
|
3
oanda/go.mod
Normal file
3
oanda/go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module github.com/fivemoreminix/autotrader/oanda
|
||||||
|
|
||||||
|
go 1.20
|
117
oanda/oanda.go
Normal file
117
oanda/oanda.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package oanda
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
auto "github.com/fivemoreminix/autotrader"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
oandaLiveURL = "https://api-fxtrade.oanda.com"
|
||||||
|
oandaPracticeURL = "https://api-fxpractice.oanda.com"
|
||||||
|
TimeLayout = time.RFC3339
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ auto.Broker = (*OandaBroker)(nil) // Compile-time interface check.
|
||||||
|
|
||||||
|
type OandaBroker struct {
|
||||||
|
*auto.SignalManager
|
||||||
|
client *http.Client
|
||||||
|
token string
|
||||||
|
accountID string
|
||||||
|
baseUrl string // Either oandaLiveURL or oandaPracticeURL.
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOandaBroker(token, accountID string, practice bool) *OandaBroker {
|
||||||
|
var baseUrl string
|
||||||
|
if practice {
|
||||||
|
baseUrl = oandaPracticeURL
|
||||||
|
} else {
|
||||||
|
baseUrl = oandaLiveURL
|
||||||
|
}
|
||||||
|
return &OandaBroker{
|
||||||
|
SignalManager: &auto.SignalManager{},
|
||||||
|
client: &http.Client{},
|
||||||
|
token: token,
|
||||||
|
accountID: accountID,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) Candles(symbol, frequency string, count int) (*auto.DataFrame, error) {
|
||||||
|
req, err := http.NewRequest("GET", b.baseUrl+"/v3/accounts/"+b.accountID+"/instruments/"+symbol+"/candles", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+b.token)
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("granularity", frequency)
|
||||||
|
q.Add("count", strconv.Itoa(auto.Min(count, 5000))) // API says max is 5000.
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
resp, err := b.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var candlestickResponse *CandlestickResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&candlestickResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDataframe(candlestickResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) MarketOrder(symbol string, units, stopLoss, takeProfit float64) (auto.Order, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) NAV() float64 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) PL() float64 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) OpenOrders() []auto.Order {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) OpenPositions() []auto.Position {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) Orders() []auto.Order {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) Positions() []auto.Position {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *OandaBroker) fetchAccountUpdates() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDataframe(candles *CandlestickResponse) (*auto.DataFrame, error) {
|
||||||
|
if candles == nil {
|
||||||
|
return nil, fmt.Errorf("candles is nil or empty")
|
||||||
|
}
|
||||||
|
data := auto.NewDOHLCVDataFrame()
|
||||||
|
for _, candle := range candles.Candles {
|
||||||
|
if candle.Mid == nil {
|
||||||
|
return nil, fmt.Errorf("mid is nil or empty")
|
||||||
|
}
|
||||||
|
var o, h, l, c float64
|
||||||
|
err := candle.Mid.Parse(&o, &h, &l, &c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing mid field of a candlestick: %w", err)
|
||||||
|
}
|
||||||
|
data.PushCandle(candle.Time, o, h, l, c, int64(candle.Volume))
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user