How to Build a Fintech Investing App w/ Alpaca API (Raven - Part 4)

This is the fourth article in the series where we are developing a demo iPhone app - Raven, utilizing the Alpaca API.

How to Build a Fintech Investing App w/ Alpaca API (Raven - Part 4)

Welcome back, let's recap!

This is the fourth article in the series where we are developing a demo iPhone app - Raven, utilizing the Alpaca API.

Alpaca - Commission-Free API First Stock Brokerage
Alpaca is a modern platform for trading. Alpaca’s API is the interface for your trading algorithms, bots, or applications to communicate with Alpaca’s brokerage and other services.

In the previous articles, we finished creating a fully functional mobile brokerage app! This means that we're able to view updated pricing information, see our positions, and buy/sell any listed asset. On top of that, we show the user updated news that we source from Polygon. Of course, there are many things that can be added to Raven such as other order types, fundamentals on a stock, better graphing features, etc., but we will leave that up to you to try out and experiment for yourselves. In this article, we will discuss the implementation of the goals setting feature. This will be a relatively naïve implementation as we want to leave the rest up to your imagination. We will allow users to set some new goals by setting a target value on that goal and by allocating some existing assets that they have to that goal. Afterward, they can see the goals that they have already set, their progress on those goals, and their overall progress across all goals.


MainViewController

We need to create another tab in our application so that the user can swipe between their HomeView and GoalsView. To do this, let's add some code to our MainViewController.

var goalsView: GoalsView!
...
@objc func left() {
        if (tabs[1].isHidden) {
            tabs[1].isHidden = false
            tabs[0].isHidden = true
            self.goalsView.progressRing.arcs[0].setProgress(CGFloat(0), duration: 0)
            
            UIView.animate(withDuration: 0.301, delay: 0.0, options: .curveEaseInOut, animations: {
                self.homeView.center.x -= self.view.frame.width
                self.goalsView.center.x -= self.view.frame.width
            }) {(true) in
                self.goalsView.setup()
            }
        }
    }
    
    @objc func right() {
        if (tabs[0].isHidden) {
            tabs[0].isHidden = false
            tabs[1].isHidden = true
        }
        
        UIView.animate(withDuration: 0.301, delay: 0.0, options: .curveEaseInOut, animations: {
            self.homeView.center.x += self.view.frame.width
            self.goalsView.center.x += self.view.frame.width
        }) {(true) in}
    }
...
self.goalsView = GoalsView(frame: CGRect(x:self.view.frame.width, y:0, width:self.view.frame.width, height: self.view.frame.height))

override func viewDidLoad() {
	...
	let leftGesture = UISwipeGestureRecognizer(target: self, action: #selector(left))
	leftGesture.direction = .left
	let rightGesture = UISwipeGestureRecognizer(target: self, action: #selector(right))
	rightGesture.direction = .right
	let tabView1 = UIImageView(frame:CGRect(x:20, y: self.view.frame.height - 40, width: self.view.frame.width/2 - 20, height: 5))
  tabView1.image = UIImage(named:"tabBar")
  let tabView2 = UIImageView(frame:CGRect(x:self.view.frame.width/2 + 10, y: self.view.frame.height - 40, width: self.view.frame.width/2 - 20, height: 5))
  tabView2.image = UIImage(named:"tabBar")
	tabs.append(tabView1)
  tabs.append(tabView2)
  tabs[1].isHidden = true
	view.addSubview(goalsView)
	view.addSubview(tabs[0])
  view.addSubview(tabs[1])
  view.addGestureRecognizer(leftGesture)
  view.addGestureRecognizer(rightGesture)
}

The tabBar that I am using is just a simple slim green rectangle. This is just to show the user which tab they are on. There are multiple ways of creating tabs so don't feel conformed to this one model.


GoalsView

Now let's define our GoalsView.

import UIKit
import ConcentricProgressRingView

class GoalsView: UIView {
    var scrollView: UIScrollView!
    var progressRing: ConcentricProgressRingView!
    var goals: [GoalView] = []
    var progressLabel: UILabel!
    
    struct Goals: Codable {
        var goals: [Goal]
        var total_progress: Float
    }
    
    struct Goal: Codable {
        var About: String
        var Target: String
        var Title: String
        var Assets: String
        var Progress: String
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.black
        
        scrollView = UIScrollView(frame: CGRect(x:0, y:50, width: self.frame.width, height: self.frame.height - 100))
        scrollView.contentSize = CGSize(width: self.frame.width-20, height: self.frame.height - 100)
        
        let header = UILabel(frame: CGRect(x:0, y: 425, width: self.frame.width, height: 50))
        header.text = "Personal Financial Goals"
        header.font = UIFont.boldSystemFont(ofSize: 25)
        header.textColor = UIColor.white
        header.textAlignment = .center
        
        let addGoalButton = UIButton(frame: CGRect(x:10, y: 800, width: frame.width - 20, height: 50))
        addGoalButton.layer.cornerRadius = 20
        addGoalButton.backgroundColor = UIColor.green
        addGoalButton.setTitle("Add Goal", for: .normal)
        addGoalButton.setTitleColor(UIColor.black, for: .normal)
        addGoalButton.addTarget(self, action: #selector(self.addGoal), for: .touchUpInside)
        
        scrollView.addSubview(header)

        let rings = [
            ProgressRing(color: UIColor.green, backgroundColor: UIColor.black, width: 40)
        ]

        let margin: CGFloat = 2
        let radius: CGFloat = 175
        progressRing = ConcentricProgressRingView(center:
            CGPoint(x: self.frame.width/2, y:225), radius: radius, margin: margin, rings: rings)
        
        progressLabel = UILabel(frame: CGRect(x: self.frame.width/2 - 60, y: 200, width: 120, height: 50))
        progressLabel.font = UIFont.boldSystemFont(ofSize: 40)
        progressLabel.textColor = UIColor.white
        progressLabel.text = "0%"
        progressLabel.textAlignment = .center
        
        scrollView.addSubview(progressRing)
        scrollView.addSubview(progressLabel)
        
        NotificationCenter.default.addObserver(self, selector: #selector(goalNotification(notification:)), name:NSNotification.Name(rawValue:"addGoal"), object: nil)
        
        addSubview(scrollView)
        addSubview(addGoalButton)
    }
    
    @objc func goalNotification(notification: NSNotification) {
        
        let title = notification.userInfo!["title"] as! String
        let about = notification.userInfo!["about"] as! String
        let progress = notification.userInfo!["progress"] as! String
        createGoal(title: title, about: about, progress: progress)
    }
    
    func createGoal(title: String, about: String, progress: String) {
        let new_goal = GoalView(frame: CGRect(x: 0, y: goals.count*80 + 500, width: 400, height: 70), progress: progress, title: title, about: about)
        
        goals.append(new_goal)
        scrollView.addSubview(new_goal)
        scrollView.contentSize = CGSize(width: 400, height: goals.count*80 + 600)
    }
    
    func animateProgress(progress: Float) {
        progressRing.arcs[0].setProgress(CGFloat(progress), duration: 0.5)
        progressLabel.text = (progress*100).description + "%"
    }
    
    @objc func addGoal() {
        let addGoalView = AddGoalsView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height))
        addSubview(addGoalView)
    }
    
    func setup() {
        let token = UserDefaults.standard.object(forKey: "token") as? String
        let url = URL(string: "http://127.0.0.1:5000/getAllGoals?token=" + token!)
        let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
        var urlRequest = URLRequest(url: url!)
        urlRequest.httpMethod = "GET"
        let dataTask = defaultSession.dataTask(with: urlRequest) { (data, response, error) in
            guard let data = data else { return }
            do {
                let vals = try JSONDecoder().decode(Goals.self, from: data)
                DispatchQueue.main.async {
                    for n in 0...vals.goals.count - 1 {
                        self.createGoal(title: vals.goals[n].Title, about: vals.goals[n].About, progress: vals.goals[n].Progress)
                    }
                    self.animateProgress(progress: vals.total_progress)
                }
            } catch let error {
                print(error)
            }
        }
        
        dataTask.resume()
    }
    
    required init?(coder: NSCoder) {
        fatalError("Fatal Error has Occurred")
    }
    
}

That was a lot of code to digest so let's go through all of it. Essentially, we're going to have one large progress bar, shown below, at the top that shows our progress across all our goals. This is calculated as the total value of the assets that we have assigned to all our goals divided by the total amount of target values across all the goals.

Most of the code in the init function is simply setting up the user interface which, again, is really up to you to decide how you want to design it. We are calling a new backend endpoint  getAllGoals so let's define that next.

@app.route('/getAllGoals')
def getGoals():
    goals = db.child('tokens').child(request.args.get('token')).child("goals").get()
    output = []
    auth = {"Authorization": "Bearer " + request.args.get('token')}
    total_target = 0
    total_achieved = 0
    for goal in goals.each():
        tickers = goal.val()["Assets"].split(" ")
        total = 0
        for ticker in tickers:
            res = json.loads(requests.get("https://paper-api.alpaca.markets/v2/positions/" + ticker, headers=auth).text)
            total += float(res["market_value"])
            
        total_achieved += total
        total_target += float(goal.val()["Target"])

        temp = dict()
        temp.update(goal.val())
        temp["Progress"] = str(total/float(goal.val()["Target"]))
        output.append(temp)

    return {"goals": output, "total_progress": round(total_achieved / total_target, 3)}

Notice how the return type is the same as the Codable structs that I have defined in my GoalsView class. Keep in mind that if you want to change the structure, you are going to have to do it in both places. Now in GoalsView, we are calling an AddGoalsView and also a GoalView. Confusing names, I know, but essentially the GoalView is a component (and should be created as such) that shows the individual goals that were created and stored in the database. This is defined as

import UIKit
import ConcentricProgressRingView

class GoalView: UIView {
    var progressRing: ConcentricProgressRingView!
    var curr_progress = 0.0
    
    init(frame: CGRect, progress: String, title: String, about: String) {
        super.init(frame: frame)
        backgroundColor = .black
        let ring = [ProgressRing(color: .green, backgroundColor: .black, width: 5)]
        progressRing = ConcentricProgressRingView(center: CGPoint(x: 30, y: frame.height/2), radius: 20, margin: 2, rings: ring)
        
        let title_label = UILabel(frame: CGRect(x: 70, y: 10, width: frame.width-80, height: 20))
        title_label.text = title
        title_label.textColor = .white
        
        let about_label = UILabel(frame: CGRect(x: 70, y: 35, width: frame.width-80, height: 20))
        about_label.text = about
        about_label.textColor = .white
        
        addSubview(progressRing)
        addSubview(title_label)
        addSubview(about_label)
        
        let bottomLine = CALayer()
        bottomLine.frame = CGRect(x: 15, y: self.frame.height - 1, width: self.frame.width-30, height: 1.0)
        bottomLine.backgroundColor = UIColor.lightText.cgColor
        layer.addSublayer(bottomLine)
        
        animateProgress(progress: (progress as NSString).floatValue)
    }
    
    func animateProgress(progress: Float) {
        progressRing.arcs[0].setProgress(CGFloat(progress), duration: 0.5)
    }
    
    required init?(coder: NSCoder) {
        fatalError("Fatal Error has Occurred")
    }
}

Now that we have that, we need to actually be able to add our goals to the database. To do this, I decided to create another view in which the user can enter a title, description, a target_value, and the assets that they want to allocate to it. There are several improvements to this model that you can try out on your own such as assigning a percentage of specific assets, adding new ones, etc. Creating a new goal might look something like this:

The implementation for the AddGoalsView with the current use case is defined below.

import UIKit

class AddGoalsView: UIView {
    var title: UITextField!
    var about: UITextField!
    var target_value: UITextField!
    var assets: UITextField!
    var bottomView: UIView!

    override init(frame: CGRect) {
        super.init(frame:frame)
        backgroundColor = UIColor.black
        layer.cornerRadius = 40
        
        let header = UILabel(frame: CGRect(x: 0, y: 40, width: self.frame.width, height: 30))
        header.text = "Create New Goal"
        header.font = UIFont.boldSystemFont(ofSize: 30)
        header.textColor = UIColor.white
        header.textAlignment = .center
        
        title = UITextField(frame: CGRect(x:20, y: 120, width: self.frame.width-20, height: 50))
        title.attributedPlaceholder = NSAttributedString(string: "Title", attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
        title.font = UIFont.systemFont(ofSize: 20)
        title.textColor = UIColor.white
        
        about = UITextField(frame: CGRect(x:20, y: 200, width: self.frame.width-20, height: 50))
        about.attributedPlaceholder = NSAttributedString(string: "Description", attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
        about.font = UIFont.systemFont(ofSize: 20)
        about.textColor = UIColor.white
        
        target_value = UITextField(frame: CGRect(x:20, y: 280, width: self.frame.width-20, height: 50))
        target_value.attributedPlaceholder = NSAttributedString(string: "Target Goal", attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
        target_value.font = UIFont.systemFont(ofSize: 20)
        target_value.textColor = UIColor.white
        
        assets = UITextField(frame: CGRect(x:20, y: 360, width: self.frame.width-20, height: 100))
        assets.attributedPlaceholder = NSAttributedString(string: "Tickers/Qty (Split by Space)" , attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
        assets.font = UIFont.systemFont(ofSize: 20)
        assets.textColor = UIColor.white
        
        let directionLabel = UILabel(frame: CGRect(x: 0, y: 25, width: self.frame.width, height: 30))
        directionLabel.text = "Swipe Up To Submit / Swipe Down to Cancel"
        directionLabel.font = UIFont.systemFont(ofSize: 15)
        directionLabel.textColor = UIColor.black
        directionLabel.textAlignment = .center
        
        let completionLabel = UILabel(frame: CGRect(x: 0, y: self.frame.height/2 - 15, width: self.frame.width, height: 30))
        completionLabel.text = "Submitted Goal!"
        completionLabel.font = UIFont.systemFont(ofSize: 30)
        completionLabel.textColor = UIColor.black
        completionLabel.textAlignment = .center
        
        bottomView = UIView(frame: CGRect(x:0, y: self.frame.height, width: self.frame.width, height: self.frame.height))
        bottomView.layer.cornerRadius = 40
        bottomView.backgroundColor = UIColor.green
        bottomView.addSubview(directionLabel)
        bottomView.addSubview(completionLabel)
        
        let swipeSubmit = UISwipeGestureRecognizer(target: self, action: #selector(submitGoal))
        swipeSubmit.direction = .up
        
        let swipeCancel = UISwipeGestureRecognizer(target: self, action: #selector(cancel))
        swipeCancel.direction = .down
        
        addSubview(bottomView)
        addSubview(header)
        addSubview(title)
        addSubview(about)
        addSubview(target_value)
        addSubview(assets)
        addGestureRecognizer(swipeSubmit)
        addGestureRecognizer(swipeCancel)
        
        UIView.animate(withDuration: 0.301, delay: 0.0, options: .curveEaseInOut, animations: {
            self.center.y += 50
            self.bottomView.center.y -= 150
        }) {(true) in}
    }
    
    @objc func cancel() {
        UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveLinear, animations: {
            self.center.y += self.frame.height
        }) {(true) in
            self.bottomView.removeFromSuperview()
            self.removeFromSuperview()
        }
    }

    @objc func submitGoal() {
        let token = UserDefaults.standard.object(forKey: "token") as? String
        let url = URL(string: "http://127.0.0.1:5000/addGoal")!
        let params: [String: Any] = [
            "token": token!,
            "title": self.title.text!,
            "about": self.about.text!,
            "target_value": self.target_value.text!,
            "assets": self.assets.text!
        ]
        var urlRequest = URLRequest(url: url)
        do {urlRequest.httpBody = try JSONSerialization.data(withJSONObject: params)
        } catch {}
        
        urlRequest.httpMethod = "POST"
        let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
            guard let data = data else { return }
            let response = String(data: data, encoding: .utf8)
            if response != "Asset Not Found" {
                DispatchQueue.main.async {
                    UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveLinear, animations: {
                        self.center.y -= self.frame.height - 50
                    }) {(true) in
                        
                        UIView.animate(withDuration: 0.3, delay: 0.5, options: .curveLinear, animations: {
                            self.center.y -= self.frame.height + 50
                        }) {(true) in
                            self.bottomView.removeFromSuperview()
                            self.removeFromSuperview()
                        }
                        
                        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "addGoal"), object: nil, userInfo: ["progress": response!, "title": self.title.text!, "about": self.about.text!, "target": self.target_value.text!])
                    }
                }
            }
        }
        dataTask.resume()
    }

    required init?(coder: NSCoder) {
        fatalError("Fatal Error has Occurred")
    }
}

Notice the new endpoint that we are using  addGoal. This is what is actually going to add our goal to the database so that we can always see it when users come back to the application.

The progress, as it stands now, will be updated every time the goalsView is loaded. The reasoning for this is that the actual percentage progress of these goals should not change in very short amounts of time as these goals are meant to be long term. Therefore, implementing constant updates is unnecessary, but if you were to target these goals for much shorter use, having a timed updater could be important. The backend endpoint is defined as:

@app.route('/addGoal', methods=["POST"])
def addGoal():
    args = list(request.form.to_dict())[0]
    args = json.loads(args)
    
    auth = {"Authorization": "Bearer " + args["token"]}
    tickers = args["assets"].split(" ")
    curr_progress = 0
    for ticker in tickers:
        res = json.loads(requests.get("https://paper-api.alpaca.markets/v2/positions/" + ticker, headers=auth).text)
        try:
            curr_progress += float(res["market_value"])
        except:
            return "Asset Not Found"
        
    new_goal = {"Title": args["title"], "About": args["about"], "Target": args["target_value"], "Assets": args["assets"]}
    db.child("tokens").child(args["token"]).child("goals").child(args["title"]).set(new_goal)

    return str(curr_progress/float(args["target_value"]))

Notice also that I am using NSNotifications again. This is just so that I can tell the GoalsView to call the backend server and display the newly added goal!

All together the goal feature might look something like this:


Wrap Up

So now we have added our own interesting feature to our brokerage app. There are plenty of more things that you can add to Raven to make it much better! Make sure to join the Alpaca Community Slack to discuss ideas, ask questions, and post your progress!

You can find the previous parts of the article series below:

How to Build a Fintech Investing App w/ Alpaca API (Raven - Pt 1)
Showing you how to build a stock trading app using Alpaca API with step-by-step code snippets. Calling our demo iPhone app “Raven”?
How to Build a Fintech Investing App w/ Alpaca API (Raven - Part 2)
The second article in our series where we will continue writing out features for our iPhone app - Raven
How to Build a Fintech Investing App w/ Alpaca API (Raven - Part 3)
This is the third article in the series where we are developing a demo iPhone app - Raven, utilizing the Alpaca API.

Until next time ✌️


Technology and services are offered by AlpacaDB, Inc. Brokerage services are provided by Alpaca Securities LLC (alpaca.markets), member FINRA/SIPC. Alpaca Securities LLC is a wholly-owned subsidiary of AlpacaDB, Inc.

You can find us @AlpacaHQ, if you use twitter.