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.
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.
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:
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.