func main() { const dateFmt = "2006-01-02" // Define our commandline flags: dbPathArg := flag.String("db", "../stocks-web/stocks.db", "Path to stocks.db database") mailServerArg := flag.String("mail-server", "localhost:25", "Address of SMTP server to use for sending email") testArg := flag.Bool("test", false, "Add test data") tmplPathArg := flag.String("template", "./emails.tmpl", "Path to email template file") // Parse the flags and set values: flag.Parse() dbPath := *dbPathArg mailutil.Server = *mailServerArg tmplPath := *tmplPathArg // Parse email template file: emailTemplate = template.Must(template.New("email").ParseFiles(tmplPath)) // Create the API context which initializes the database: api, err := stocks.NewAPI(dbPath) if err != nil { log.Fatalln(err) return } defer api.Close() // Testing data: if *testArg { testUser := &stocks.User{ Name: "Test User", NotificationTimeout: time.Minute, Emails: []stocks.UserEmail{ stocks.UserEmail{Email: "*****@*****.**", IsPrimary: true}, }, } err := api.AddUser(testUser) if err == nil { // Real data from market: s := &stocks.Stock{ UserID: testUser.UserID, Symbol: "MSFT", BuyDate: stocks.ToDateTime(dateFmt, "2013-09-03"), BuyPrice: stocks.ToDecimal("31.88"), Shares: 10, TStopPercent: stocks.ToNullDecimal("2.50"), } api.AddStock(s) s = &stocks.Stock{ UserID: testUser.UserID, Symbol: "MSFT", BuyDate: stocks.ToDateTime(dateFmt, "2013-09-03"), BuyPrice: stocks.ToDecimal("31.88"), Shares: -5, TStopPercent: stocks.ToNullDecimal("2.50"), } api.AddStock(s) s = &stocks.Stock{ UserID: testUser.UserID, Symbol: "AAPL", BuyDate: stocks.ToDateTime(dateFmt, "2013-09-03"), BuyPrice: stocks.ToDecimal("488.58"), Shares: 10, TStopPercent: stocks.ToNullDecimal("2.50"), } api.AddStock(s) s = &stocks.Stock{ UserID: testUser.UserID, Symbol: "AAPL", BuyDate: stocks.ToDateTime(dateFmt, "2013-09-03"), BuyPrice: stocks.ToDecimal("488.58"), Shares: -5, TStopPercent: stocks.ToNullDecimal("2.50"), } api.AddStock(s) s = &stocks.Stock{ UserID: testUser.UserID, Symbol: "YHOO", BuyDate: stocks.ToDateTime(dateFmt, "2013-09-03"), BuyPrice: stocks.ToDecimal("31.88"), Shares: 0, IsWatched: true, TStopPercent: stocks.ToNullDecimal("2.50"), } api.AddStock(s) } } // Query stocks: symbols, err := api.GetAllTrackedSymbols() if err != nil { log.Fatalln(err) return } // Run through each actively tracked stock and calculate stopping prices, notify next of kin, what have you... log.Printf("%d stocks tracked.\n", len(symbols)) for _, symbol := range symbols { // Record trading history: log.Printf(" %s: recording historical data and calculating statistics...\n", symbol) api.RecordHistory(symbol) } // Fetch current prices from Yahoo into the database: log.Printf("Fetching current prices...\n") api.GetCurrentHourlyPrices(true, symbols...) for _, symbol := range symbols { // Calculate details of owned stocks and their owners for this symbol: details, err := api.GetStockDetailsForSymbol(symbol) if err != nil { panic(err) } for _, sd := range details { s := &sd.Stock d := &sd.Detail // Get the owner: user, err := api.GetUser(s.UserID) if err != nil { panic(err) } log.Printf(" %s\n", symbol) if !sd.Stock.IsWatched { log.Printf(" %s bought %d shares at %s on %s:\n", user.Name, s.Shares, s.BuyPrice, s.BuyDate.DateString()) } else { log.Printf(" %s watching from %s on %s:\n", user.Name, s.BuyPrice, s.BuyDate.DateString()) } if sd.Detail.CurrPrice.Valid { log.Printf(" current: %v\n", sd.Detail.CurrPrice) } if d.TStopPrice.Valid { log.Printf(" t-stop: %v\n", d.TStopPrice) } if d.GainLossDollar.Valid { log.Printf(" gain($): %v\n", d.GainLossDollar.CurrencyString()) } if d.GainLossPercent.Valid { log.Printf(" gain(%%): %v\n", d.GainLossPercent) } // Check notifications: log.Println() checkTStop(api, user, &sd) checkBuyStop(api, user, &sd) checkSellStop(api, user, &sd) checkRise(api, user, &sd) checkFall(api, user, &sd) checkBullBear(api, user, &sd) } } log.Println("Job complete") return }
// Handles /ui/* requests to present HTML UI to the user: func uiHandler(w http.ResponseWriter, r *http.Request) { // Get API ready: api, err := stocks.NewAPI(dbPath) if err != nil { log.Println(err) http.Error(w, "Could not open stocks database!", http.StatusInternalServerError) return } defer api.Close() // Handle panic()s as '500' responses: defer func() { if err := recover(); err != nil { log.Println(err) if bad, ok := err.(BadRequestError); ok { http.Error(w, bad.Error(), http.StatusBadRequest) } else { http.Error(w, "Internal server error", http.StatusInternalServerError) } return } // Normal execution. }() // Find user: webuser := getUserData(r) apiuser, err := api.GetUserByEmail(webuser.Email) if apiuser == nil || err != nil { if r.URL.Path != "/register" { http.Redirect(w, r, "/ui/register", http.StatusFound) return } } // Handle request: switch r.URL.Path { case "/register": if r.Method == "GET" { // Data to be used by the template: model := struct { WebUser *UserCookieData User *stocks.User }{ WebUser: webuser, User: apiuser, } uiTmpl.ExecuteTemplate(w, "register", model) } else { // Assume POST. // Assume apiuser == nil, implying webuser.Email not found in database. // Add user: apiuser = &stocks.User{ Name: webuser.FullName, NotificationTimeout: time.Hour * time.Duration(24), Emails: []stocks.UserEmail{ stocks.UserEmail{ Email: webuser.Email, IsPrimary: true, }, }, } err = api.AddUser(apiuser) panicIf(err) http.Redirect(w, r, "/ui/dash", http.StatusFound) } return // ------------------------------------------------- case "/dash": // Fetch data to be used by the template: owned, watched := getDetailsSplit(api, apiuser.UserID) model := struct { User *stocks.User Owned []stocks.StockDetail Watched []stocks.StockDetail }{ User: apiuser, Owned: owned, Watched: watched, } err := uiTmpl.ExecuteTemplate(w, "dash", model) panicIf(err) return case "/fetch": // Fetch latest data: // Query stocks: symbols, err := api.GetAllTrackedSymbols() panicIf(err) fetchLatest(api, symbols...) // Redirect to dashboard with updated data: http.Redirect(w, r, "/ui/dash", http.StatusFound) return // ------------------------------------------------- case "/owned/add": if r.Method == "GET" { // Data to be used by the template: model := struct { User *stocks.User Today time.Time IsWatched bool }{ User: apiuser, Today: time.Now(), IsWatched: false, } err := uiTmpl.ExecuteTemplate(w, "add", model) panicIf(err) return } case "/watched/add": if r.Method == "GET" { // Data to be used by the template: model := struct { User *stocks.User Today time.Time IsWatched bool }{ User: apiuser, Today: time.Now(), IsWatched: true, } err := uiTmpl.ExecuteTemplate(w, "add", model) panicIf(err) return } // ------------------------------------------------- case "/stock/edit": if r.Method == "GET" { // Data to be used by the template: id := r.URL.Query().Get("id") st, err := api.GetStock(stocks.StockID(tryParseInt(id, "id query string parameter is required"))) panicIf(err) if st == nil { http.Error(w, "404 Not Found", http.StatusNotFound) return } // Security check. if st.UserID != apiuser.UserID { http.Error(w, "404 Not Found", http.StatusNotFound) return } model := struct { User *stocks.User StockJSON string IsWatched bool }{ User: apiuser, StockJSON: toJSON(st), IsWatched: st.IsWatched, } // Render the appropriate html template: err = uiTmpl.ExecuteTemplate(w, "edit", model) panicIf(err) return } } http.NotFound(w, r) return }
// Handles /api/* requests for JSON API: func apiHandler(w http.ResponseWriter, r *http.Request) { // Get authenticated user data: webuser := getUserData(r) // Set these values in the API switch handler below: var rspcode int = 200 var rsperr error var rsp interface{} // Send JSON response at end: defer func() { // Determine if error or success: if err := recover(); err != nil { if bad, ok := err.(BadRequestError); ok { rsperr = bad rspcode = 400 } else { log.Println(err) rsperr = fmt.Errorf("Internal server error") } } var jrsp jsonResponse if rsperr != nil { // Error response: bytes, err := json.Marshal(rsperr.Error()) if err != nil { log.Println(err) http.Error(w, "Error marshaling JSON error respnose", http.StatusInternalServerError) return } jrsp = jsonResponse{ Success: false, Error: new(json.RawMessage), Result: &null, } *jrsp.Error = json.RawMessage(bytes) // Default to 500 error if unspecified: if rspcode == 200 { rspcode = 500 } } else { // Success response: bytes, err := json.Marshal(rsp) if err != nil { log.Println(err) http.Error(w, "Error marshaling JSON success respnose", http.StatusInternalServerError) return } jrsp = jsonResponse{ Success: true, Error: &null, Result: new(json.RawMessage), } *jrsp.Result = json.RawMessage(bytes) } // Marshal the root JSON response structure to a []byte: bytes, err := json.Marshal(jrsp) if err != nil { log.Println(err) http.Error(w, "Error marshaling root JSON respnose", http.StatusInternalServerError) return } // Send the JSON response: w.Header().Set("Content-Type", `application/json; charset="utf-8"`) w.WriteHeader(rspcode) w.Write(bytes) }() // Open API database: api, err := stocks.NewAPI(dbPath) if err != nil { log.Println(err) http.Error(w, "Could not open stocks database!", http.StatusInternalServerError) return } defer api.Close() // Get API user: apiuser, err := api.GetUserByEmail(webuser.Email) if err != nil { log.Println(err) } // Handle API urls: if r.Method == "GET" { // GET switch r.URL.Path { case "/user/who": rsp = apiuser case "/owned/list": // Get list of owned stocks w/ details. owned, _ := getDetailsSplit(api, apiuser.UserID) rsp = owned case "/watched/list": // Get list of watched stocks w/ details. _, watched := getDetailsSplit(api, apiuser.UserID) rsp = watched case "/stock/price": // Get current price of stock: symbol := r.URL.Query().Get("symbol") if symbol == "" { rspcode = 400 rsperr = fmt.Errorf("Required symbol parameter") return } symbol = strings.Trim(strings.ToUpper(symbol), " ") // Get the last hourly price for the symbol: prices := api.GetCurrentHourlyPrices(true, symbol) // Return the price value: rsp = struct { Symbol string Price stocks.Decimal }{ Symbol: symbol, Price: prices[symbol], } default: rspcode = 404 rsperr = fmt.Errorf("Invalid API url") } } else { // POST switch r.URL.Path { case "/user/register": // Register new user with primary email. rsperr = fmt.Errorf("TODO") case "/user/join": // Join to existing user with secondary email. rsperr = fmt.Errorf("TODO") case "/stock/add": // Add stock. // Parse body as JSON: tmp := struct { Symbol string BuyDate string BuyPrice string Shares int64 IsWatched bool TStopPercent string BuyStopPrice string SellStopPrice string RisePercent string FallPercent string NotifyBullBear bool }{} parsePostJson(r, &tmp) // Validate settings and respond 400 if failed: validate(tmp.Symbol != "", "Symbol required") validate(tmp.BuyDate != "", "BuyDate required") validate(tmp.BuyPrice != "", "BuyPrice required") // Convert JSON input into stock struct: s := &stocks.Stock{ UserID: apiuser.UserID, Symbol: strings.Trim(strings.ToUpper(tmp.Symbol), " "), BuyDate: stocks.ToDateTime(dateFmt, strings.Trim(tmp.BuyDate, " ")), BuyPrice: stocks.ToDecimal(strings.Trim(tmp.BuyPrice, " ")), Shares: tmp.Shares, IsWatched: tmp.IsWatched, TStopPercent: stocks.ToNullDecimal(tmp.TStopPercent), BuyStopPrice: stocks.ToNullDecimal(tmp.BuyStopPrice), SellStopPrice: stocks.ToNullDecimal(tmp.SellStopPrice), RisePercent: stocks.ToNullDecimal(tmp.RisePercent), FallPercent: stocks.ToNullDecimal(tmp.FallPercent), NotifyBullBear: tmp.NotifyBullBear, } // Enable/disable notifications based on what's filled out: if s.TStopPercent.Valid { s.NotifyTStop = true } if s.BuyStopPrice.Valid { s.NotifyBuyStop = true } if s.SellStopPrice.Valid { s.NotifySellStop = true } if s.RisePercent.Valid { s.NotifyRise = true } if s.FallPercent.Valid { s.NotifyFall = true } // Add the stock record: err = api.AddStock(s) panicIf(err) // Check if we have to recreate history: minBuyDate := api.GetMinBuyDate(s.Symbol) if minBuyDate.Valid && s.BuyDate.Value.Before(minBuyDate.Value) { // Introducing earlier BuyDates screws up the TradeDayIndex values. // TODO(jsd): We could probably fix this better by simply reindexing the TradeDayIndex values and filling in the holes. api.DeleteHistory(s.Symbol) } // Fetch latest data for new symbol: fetchLatest(api, s.Symbol) rsp = "ok" case "/stock/update": // Update stock. // Parse body as JSON: tmp := struct { StockID int64 Symbol string BuyDate string BuyPrice string Shares int64 IsWatched bool TStopPercent string NotifyTStop bool BuyStopPrice string NotifyBuyStop bool SellStopPrice string NotifySellStop bool RisePercent string NotifyRise bool FallPercent string NotifyFall bool NotifyBullBear bool }{} parsePostJson(r, &tmp) // Validate settings and respond 400 if failed: validate(tmp.BuyPrice != "", "BuyPrice required") // Get stock from the database: s, err := api.GetStock(stocks.StockID(tmp.StockID)) panicIf(err) // 404 if wrong user attempts to update: if s.UserID != apiuser.UserID { rspcode = 404 rsperr = fmt.Errorf("Not Found") return } // Convert JSON input into stock struct: s.BuyPrice = stocks.ToDecimal(tmp.BuyPrice) s.Shares = tmp.Shares s.IsWatched = tmp.IsWatched s.TStopPercent = stocks.ToNullDecimal(tmp.TStopPercent) s.NotifyTStop = tmp.NotifyTStop s.BuyStopPrice = stocks.ToNullDecimal(tmp.BuyStopPrice) s.NotifyBuyStop = tmp.NotifyBuyStop s.SellStopPrice = stocks.ToNullDecimal(tmp.SellStopPrice) s.NotifySellStop = tmp.NotifySellStop s.RisePercent = stocks.ToNullDecimal(tmp.RisePercent) s.NotifyRise = tmp.NotifyRise s.FallPercent = stocks.ToNullDecimal(tmp.FallPercent) s.NotifyFall = tmp.NotifyFall s.NotifyBullBear = tmp.NotifyBullBear // Add the stock record: err = api.UpdateStock(s) panicIf(err) rsp = "ok" case "/stock/remove": tmp := struct { ID int64 `json:"id"` }{} parsePostJson(r, &tmp) stockID := stocks.StockID(tmp.ID) st, err := api.GetStock(stockID) if st == nil { rsp = "ok" return } // Security check. if st.UserID != apiuser.UserID { http.Error(w, "404 Not Found", http.StatusNotFound) return } err = api.RemoveStock(stockID) panicIf(err) rsp = "ok" default: rspcode = 404 rsperr = fmt.Errorf("Invalid API url") } } }