예제 #1
0
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
}
예제 #2
0
// 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
}
예제 #3
0
// 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")
		}
	}
}