Skip to content

Commit

Permalink
Merge pull request #9 from ianmcmahon/auth
Browse files Browse the repository at this point in the history
Auth
  • Loading branch information
ianmcmahon committed Dec 9, 2020
2 parents 94ee125 + 04e6af5 commit 63c1bd9
Show file tree
Hide file tree
Showing 11 changed files with 23,919 additions and 42 deletions.
140 changes: 137 additions & 3 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import (
"net/url"
"os"
"path"
"strings"
"sync"
"time"

"gopkg.in/headzoo/surf.v1"
)

var tokenEndpoint string = "https://api.tdameritrade.com/v1/oauth2/token"
Expand All @@ -38,7 +41,7 @@ func (c *Client) SetToken(token *TokenResponse) error {

f, err := os.Create(path.Join(configDir, "tdam_refresh"))
if err != nil {
return nil
return err
}
defer f.Close()
_, err = f.Write([]byte(token.RefreshToken))
Expand All @@ -64,7 +67,12 @@ func (c *Client) TDAMToken() (string, error) {
refresh, err := getStoredRefreshToken()
if err != nil || refresh == "" {
fmt.Printf("Error fetching refresh token\n")
return "", err

err := c.slogThroughOauthFlow()
if err != nil {
fmt.Println(err)
return "", err
}
}
tdamToken, err = c.refreshToken(refresh)
if err != nil {
Expand All @@ -83,6 +91,10 @@ func (c *Client) TdamAuthURL() string {
}

func getStoredRefreshToken() (string, error) {
envToken := os.Getenv("REFRESH_TOKEN")
if envToken != "" {
return envToken, nil
}
configDir, err := getConfigDir()
if err != nil {
return "", err
Expand Down Expand Up @@ -128,7 +140,7 @@ func (c *Client) AuthHandler(w http.ResponseWriter, req *http.Request) {
fmt.Printf("error setting token: %v\n", err)
fmt.Fprintf(w, "error setting token: %v\n", err)
} else {
fmt.Fprintf(w, "token acquired! have fun")
fmt.Fprintf(w, "token acquired! have fun: %v", token)
}
}

Expand Down Expand Up @@ -210,3 +222,125 @@ func (c *Client) refreshToken(code string) (*TokenResponse, error) {

return &token, err
}

func (c *Client) slogThroughOauthFlow() error {
username := os.Getenv("TDAM_USERNAME")
password := os.Getenv("TDAM_PASSWORD")
if username == "" || password == "" {
return fmt.Errorf("Must set TDAM_USERNAME and TDAM_PASSWORD for automatic oauth\n")
}
fmt.Printf("hang on, let's try to do this ourselves\n")

bow := surf.NewBrowser()
err := bow.Open(c.TdamAuthURL())
if err != nil {
return err
}
fmt.Println(bow.Title())

title, err := bow.Find("div.title>h1>p").First().Html()
if err != nil {
return err
}
fmt.Printf("step 1 title: %#v\n", title)
// at this point, title should be "Secure Log-in"
// this title is wrapped in a <p> but 2fa pages aren't

// Log in to the site.
fm, err := bow.Form("#authform")
if err != nil {
return err
}
fm.Input("su_username", username)
fm.Input("su_password", password)
if err := fm.Submit(); err != nil {
return err
}
_ = bow.Body() // the below doesn't seem to work unless we read the body. Is there a better way?
//fmt.Println(bow.Body())

title, err = bow.Find("div.title>h1").First().Html()
if err != nil {
return err
}
fmt.Printf("step 2 title: %#v\n", title)
// here, we either are at a 2fa page or the final page
// 2fa would be "Get Code via Text Message"

if title == "Get Code via Text Message" {
fmt.Printf("lets try to use the security question\n")
fm, err := bow.Form("#authform")
if err != nil {
return err
}
if err := fm.Click("init_secretquestion"); err != nil {
return err
}
_ = bow.Body()
//fmt.Println(bow.Body())
title, err := bow.Find("div.title>h1").First().Html()
if err != nil {
return err
}
fmt.Printf("step 2b title: %#v\n", title)
// here, we should have the title "Answer Security Question'"
desc, err := bow.Find("div.description>p").Last().Html()
if err != nil {
return err
}
fmt.Printf("step 2b desc: %#v\n", desc)

env := ""
if strings.Contains(desc, "What is your paternal grandmother&#39;s first name?") {
env = "TDAM_SQ_ANSWER_1"
}
if strings.Contains(desc, "What is your maternal grandmother&#39;s first name?") {
env = "TDAM_SQ_ANSWER_2"
}
if strings.Contains(desc, "What was the name of the town your grandmother lived in?") {
env = "TDAM_SQ_ANSWER_3"
}
if strings.Contains(desc, "What is your best friend&#39;s first name?") {
env = "TDAM_SQ_ANSWER_4"
}
if env == "" {
return fmt.Errorf("Unknown security question: %#v", desc)
}
sqAnswer := os.Getenv(env)
if sqAnswer == "" {
return fmt.Errorf("Please provide the answer to the following question in %s: %#v", env, desc)
}

// ideally we should check the security question but i've only gotten one so far
fm, err = bow.Form("#authform")
if err != nil {
return err
}
fm.Input("su_secretquestion", sqAnswer)
if err := fm.Submit(); err != nil {
return err
}
_ = bow.Body()
// fmt.Println(bow.Body())
}

// at this point, we should be at "TD Ameritrade Authorization"
// which shows us the scope and asks us to accept
title, err = bow.Find("div.title>h1").First().Html()
if err != nil {
return err
}
fmt.Printf("step 3 title: %#v\n", title)

// set the transport to ignore https cert validation
bow.SetTransport(&http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
})

if err := fm.Submit(); err != nil {
return err
}
fmt.Println(bow.Body()) // this should come from our callback server

return nil
}
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ module github.com/ianmcmahon/tdam

go 1.13

require github.com/gorilla/websocket v1.4.1
require (
github.com/PuerkitoBio/goquery v1.6.0 // indirect
github.com/gorilla/websocket v1.4.1
github.com/headzoo/surf v1.0.0
gopkg.in/headzoo/surf.v1 v1.0.0
)
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
github.com/PuerkitoBio/goquery v1.6.0 h1:j7taAbelrdcsOlGeMenZxc2AWXD5fieT1/znArdnx94=
github.com/PuerkitoBio/goquery v1.6.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/headzoo/surf v1.0.0 h1:d2h9ftKeQYj7tKqAjQtAA0lJVkO8cTxvzdXLynmNnHM=
github.com/headzoo/surf v1.0.0/go.mod h1:/bct0m/iMNEqpn520y01yoaWxsAEigGFPnvyR1ewR5M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/headzoo/surf.v1 v1.0.0 h1:Ti4LagTvHxSdHYHf5DTqJRhY4+pQYZ0slBPlxo2IWGU=
gopkg.in/headzoo/surf.v1 v1.0.0/go.mod h1:T0BH8276y+OPL0E4tisxCFjBVIAKGbwdYU7AS7/EpQQ=
17 changes: 13 additions & 4 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"net/http"
"net/url"
"time"
"io/ioutil"
"bytes"

"github.com/ianmcmahon/tdam"
)
Expand Down Expand Up @@ -59,9 +61,6 @@ func (c *Client) GetChain(symbol string, options url.Values) (*OptionChain, erro
options.Set("symbol", symbol)
req.URL.RawQuery = options.Encode()

//dump, _ := httputil.DumpRequest(req, false)
//fmt.Println(string(dump))

resp, err := client.Do(req)
if err != nil {
fmt.Printf("%v\n", err)
Expand All @@ -71,8 +70,18 @@ func (c *Client) GetChain(symbol string, options url.Values) (*OptionChain, erro
fmt.Printf("status %d: %s\n", resp.StatusCode, resp.Status)
}

body, _ := ioutil.ReadAll(resp.Body)
body = bytes.Replace(body, []byte("\"NaN\""), []byte("null"), -1)

var chain OptionChain
if err := json.NewDecoder(resp.Body).Decode(&chain); err != nil {
if err := json.NewDecoder(bytes.NewBuffer(body)).Decode(&chain); err != nil {
tmpdir, _ := ioutil.TempDir("/tmp", "tdam-debug-*")
tmpfile, _ := ioutil.TempFile(tmpdir, "*.json")
n, errr := tmpfile.Write(body)
if errr != nil {
fmt.Printf("error writing dumpfile: %v\n", errr)
}
fmt.Printf("logged (%d bytes) bad json to %s\n", n, tmpfile.Name())
return nil, err
}

Expand Down
28 changes: 28 additions & 0 deletions options/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package options

import (
"encoding/json"
"bytes"
"os"
"testing"
"io/ioutil"
)

func TestOptionChainSchema(t *testing.T) {
//f, err := os.Open("testdata/spy_options_response.json")
f, err := os.Open("testdata/nan_test.json")
if err != nil {
t.Error(err)
}

buf, err := ioutil.ReadAll(f)
if err != nil {
t.Error(err)
}
buf = bytes.Replace(buf, []byte("\"NaN\""), []byte("null"), -1)

var chain OptionChain
if err := json.NewDecoder(bytes.NewBuffer(buf)).Decode(&chain); err != nil {
t.Error(err)
}
}
54 changes: 42 additions & 12 deletions options/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ func (c *OptionChain) StrikeTable(exp ExpirationDate) StrikeTable {
return table
}

type EpochTime time.Time

func (t EpochTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%d", time.Time(t).Unix())), nil
}

func (t *EpochTime) UnmarshalJSON(b []byte) error {
secs, err := strconv.ParseUint(string(b), 10, 64)
if err != nil {
return err
}
*(*time.Time)(t) = time.Unix(int64(secs), 0)
return nil
}

type Option struct {
PutCall string `json:"putCall"`
Symbol string `json:"symbol"`
Expand All @@ -71,12 +86,12 @@ type Option struct {
QuoteTimeInLong float64 `json:"quoteTimeInLong"`
TradeTimeInLong float64 `json:"tradeTimeInLong"`
NetChange float64 `json:"netChange"`
Volatility float64 `json:"volatility"`
Delta float64 `json:"delta"`
Gamma float64 `json:"gamma"`
Theta float64 `json:"theta"`
Vega float64 `json:"vega"`
Rho float64 `json:"rho"`
Volatility float64 `json:"volatility"`
Delta float64 `json:"delta"`
Gamma float64 `json:"gamma"`
Theta float64 `json:"theta"`
Vega float64 `json:"vega"`
Rho float64 `json:"rho"`
TimeValue float64 `json:"timeValue"`
OpenInterest float64 `json:"openInterest"`
IsInTheMoney bool `json:"isInTheMoney"`
Expand All @@ -86,7 +101,7 @@ type Option struct {
IsNonStandard bool `json:"isNonStandard"`
OptionDeliverablesList []OptionDeliverables `json:"optionDeliverablesList"`
StrikePrice float64 `json:"strikePrice"`
ExpirationDate string `json:"expirationDate"`
ExpirationDate EpochTime `json:"expirationDate"`
ExpirationType string `json:"expirationType"`
Multiplier float64 `json:"multiplier"`
SettlementType string `json:"settlementType"`
Expand All @@ -97,6 +112,21 @@ type Option struct {
MarkPercentChange float64 `json:"markPercentChange"`
}

type NaNableFloat64 float64

func (v *NaNableFloat64) UnmarshalText(b []byte) error {
if string(b) == "NaN" {
*v = NaNableFloat64(math.NaN())
return nil
}
f, err := strconv.ParseFloat(string(b), 64)
if err != nil {
return err
}
*v = NaNableFloat64(f)
return nil
}

type StrikePrice float64

func (s StrikePrice) String() string {
Expand Down Expand Up @@ -126,12 +156,12 @@ func (s StrikeTable) NearestToDelta(d float64) (put, call Strike) {

for _, strike := range s {
if strike.DistToDelta(d) < put.DistToDelta(d) {
if math.Abs(strike.Put[0].Delta)-d < math.Abs(strike.Call[0].Delta)-d {
if math.Abs(float64(strike.Put[0].Delta))-d < math.Abs(float64(strike.Call[0].Delta))-d {
put = strike
}
}
if strike.DistToDelta(d) < call.DistToDelta(d) && strike.Call[0].Delta > 0 {
if math.Abs(strike.Put[0].Delta)-d > math.Abs(strike.Call[0].Delta)-d {
if math.Abs(float64(strike.Put[0].Delta))-d > math.Abs(float64(strike.Call[0].Delta))-d {
call = strike
}
}
Expand All @@ -143,15 +173,15 @@ func (s StrikeTable) NearestToDelta(d float64) (put, call Strike) {
func (s Strike) DistToDelta(d float64) float64 {
c := s.Call[0]
p := s.Put[0]
return math.Abs(math.Min(math.Abs(c.Delta)-d, math.Abs(p.Delta)-d))
return math.Abs(math.Min(math.Abs(float64(c.Delta))-d, math.Abs(float64(p.Delta))-d))
}

func (s Strike) DeltaAbove(d float64) bool {
return math.Abs(s.Call[0].Delta) > d && math.Abs(s.Put[0].Delta) > d
return math.Abs(float64(s.Call[0].Delta)) > d && math.Abs(float64(s.Put[0].Delta)) > d
}

func (s Strike) DeltaBelow(d float64) bool {
return math.Abs(s.Call[0].Delta) < d || math.Abs(s.Put[0].Delta) < d
return math.Abs(float64(s.Call[0].Delta)) < d || math.Abs(float64(s.Put[0].Delta)) < d
}

func (s Strike) DeltaBetween(a, b float64) bool {
Expand Down
1 change: 1 addition & 0 deletions options/testdata/880587956.json

Large diffs are not rendered by default.

Loading

0 comments on commit 63c1bd9

Please sign in to comment.