Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pipes - Kee - Ada Trader #36

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
4 changes: 2 additions & 2 deletions dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ <h3 class="price">$<%- price.toFixed(2) %></h3>
<script type="text/template" id="trade-template">
<li class="trade">
<span>
You <%- buy ? 'bought' : 'sold' %> <%- symbol %> at $<%- price.toFixed(2) %>
You <%- buy %> <%- symbol %> at $<%- price.toFixed(2) %>
</span>
</li>
</script>

<script type="text/template" id="order-template">
<h3 class="symbol"><%- symbol %></h3>
<div class="detail">
<span class="action"><%- buy ? 'buy' : 'sell' %></span>
<span class="action"><%- buy %></span>
at
<span class="price">$<%- targetPrice.toFixed(2) %></span>
</div>
Expand Down
71 changes: 71 additions & 0 deletions spec/models/order_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Order from 'models/order';
import Quote from 'models/quote';

describe('Order spec', () => {
let order;
let quote;
beforeEach(() => {
quote = new Quote({
symbol: 'DOGS',
price: 66,
});
});

describe('Need a targetPrice', () => {
it('is invalid without a targetPrice', () => {
order = new Order({
symbol: 'DOGS',
buy: 'buy',
quote: quote,
})

expect(order.isValid()).toBeFalsy();
});
it('is valid with a targetPrice', () => {
order = new Order({
symbol: 'DOGS',
targetPrice: 55,
buy: 'buy',
quote: quote,
})

expect(order.isValid()).toBeTruthy();
});
});

describe('Need valid targetPrice', () => {
it('(BUY) must have targetPrice lower than trade price', () => {
order = new Order({
symbol: 'DOGS',
targetPrice: 999,
buy: "buy",
quote: quote,
})
expect(order.isValid()).toBeFalsy();

order.set('targetPrice', 66)
expect(order.isValid()).toBeFalsy();

order.set('targetPrice', 55)
expect(order.isValid()).toBeTruthy();
});

it('(SELL) must have a targetPrice higher than trade price', () => {
order = new Order({
symbol: 'DOGS',
targetPrice: 44,
buy: "sell",
quote: quote,
})
expect(order.isValid()).toBeFalsy();

order.set('targetPrice', 66)
expect(order.isValid()).toBeFalsy();

order.set('targetPrice', 77)
expect(order.isValid()).toBeTruthy();
});

});

});
68 changes: 66 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
// CSS
import 'foundation-sites/dist/foundation.css';
import 'css/app.css';

// Modules
import $ from 'jquery';
import _ from 'underscore';

import Simulator from 'models/simulator';
import QuoteList from 'collections/quote_list';
// Imports
import Simulator from './models/simulator';
import QuoteList from './collections/quote_list';
import OrderList from './collections/order_list';

import QuoteView from './views/quote_view';
import QuoteListView from './views/quote_list_view';
import TradeView from './views/trade_view';
import OrderView from './views/order_view';
import OrderListView from './views/order_list_view';

// -------------------------------------------------------

// Given quotes
const quoteData = [
{
symbol: 'HUMOR',
Expand All @@ -25,11 +39,61 @@ const quoteData = [
},
];

// Got quote names & prices into separate arrays
const names = quoteData.map(info => info.symbol);
const prices = quoteData.map(info => info.price);

// Define some variables
let quoteTemplate;
let tradeTemplate;
let orderTemplate;

// -------------------------------------------------------

// jQuery Ready
$(document).ready(function() {

// Event Bus
let bus = {};
bus = _.extend(bus, Backbone.Events);

// Templates
quoteTemplate = _.template($('#quote-template').html());
tradeTemplate = _.template($('#trade-template').html());
orderTemplate = _.template($('#order-template').html());

// Constants
const quotes = new QuoteList(quoteData);
const simulator = new Simulator({
quotes: quotes,
});
const orders = new OrderList();

// Render quote list view
const quoteListView = new QuoteListView({
el: $('#quotes-container'),
model: quotes,
template: quoteTemplate,
bus: bus,
});

// Render trade history view
const tradeView = new TradeView({
el: $('#trades-container'),
template: tradeTemplate,
bus: bus,
})

// Render order list view
const orderListView = new OrderListView({
el: $('#order-workspace'),
model: orders,
template: orderTemplate,
quoteList: quotes,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that the OrderListView knows about the QuoteList. I've seen several students attempt to achieve the same thing using the bus as an intermediary, but the code ends up being much more complex.

bus: bus,
});

quoteListView.render();
orderListView.render();
simulator.start();
});
8 changes: 8 additions & 0 deletions src/collections/order_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Backbone from 'backbone';
import Order from 'models/order';

const OrderList = Backbone.Collection.extend({
model: Order,
});

export default OrderList;
30 changes: 30 additions & 0 deletions src/models/order.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Backbone from 'backbone';

const Order = Backbone.Model.extend({
validate(attributes) {
const errors = {};

if (!attributes.targetPrice) {
errors['targetPrice'] = 'A target price is required.';
}

if(attributes.buy == "buy" &&
attributes.targetPrice >= attributes.quote.get('price')) {
errors['targetPrice'] = ['The target price must be lower than the trade price'];
}

if(attributes.buy === "sell" &&
attributes.targetPrice <= attributes.quote.get('price')){
errors['targetPrice'] = ['The target price must be higher than the trade price'];
}

if ( Object.keys(errors).length > 0 ) {
return errors;
} else {
return false;
}

},
});

export default Order;
10 changes: 8 additions & 2 deletions src/models/quote.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@ const Quote = Backbone.Model.extend({
},

buy() {
// Implement this function to increase the price by $1.00
let current_price = this.get('price');
this.set('price', current_price += 1);
let current_symbol = this.get('symbol');
return { symbol: current_symbol, price: current_price, buy: 'bought' };
},

sell() {
// Implement this function to decrease the price by $1.00
let current_price = this.get('price');
this.set('price', current_price -= 1);
let current_symbol = this.get('symbol');
return { symbol: current_symbol, price: current_price, buy: 'sold' };
},
});

Expand Down
108 changes: 108 additions & 0 deletions src/views/order_list_view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Backbone from 'backbone';
import OrderView from './order_view';
import Order from '../models/order';

const OrderListView = Backbone.View.extend({
initialize(params) {
this.template = params.template;
this.bus = params.bus;
this.quotes = params.quoteList;

this.listenTo(this.model, 'update', this.render);
this.listenTo(this.bus, 'quote_change', this.buyOrSellOrder);

this.displayOrderForm();
},

render() {
this.$('#orders').empty();

this.model.forEach((order) => {
const orderView = new OrderView({
model: order,
template: this.template,
tagName: 'li',
className: 'order',
bus: this.bus,
});
this.$('#orders').append(orderView.render().$el);
});

return this;
},

events: {
'click button.btn-buy': 'buyOrder',
'click button.btn-sell': 'sellOrder',
},

displayOrderForm() {
this.quotes.each((quote) => {
const symbol = quote.get('symbol');
this.$('.order-entry-form select').append(`<option value="${symbol}">${symbol}</option>`);
});
},

getFormData(action) {
const orderData = {};

orderData['symbol'] = this.$('.order-entry-form select[name="symbol"] option:selected').val();
orderData['quote'] = this.quotes.findWhere({symbol: orderData['symbol']});
orderData['targetPrice'] = Number(this.$('.order-entry-form input[name="price-target"]').val());
orderData['buy'] = action;

return orderData;
},

clearFormData() {
this.$('.order-entry-form input[name="price-target"]').val('');
},

buyOrder(event) {
this.addOrder(event, 'buy');
},

sellOrder(event) {
this.addOrder(event, 'sell');
},

addOrder(event, action) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work using a helper method to DRY up your code here!

event.preventDefault();

const formData = this.getFormData(action);

const newOrder = new Order(formData);
this.model.add(newOrder);
this.clearFormData();
},

buyOrSellOrder(quote) {
let currentOrders = this.model.where({symbol: quote.get('symbol')});
currentOrders.forEach((order) => {

if (order.get('buy') == 'buy' && order.get('targetPrice') >= quote.get('price')) {
let tradeItem = {
price: order.get('targetPrice'),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit of logic inside the forEach loop would work well as a function defined on Order.

symbol: order.get('symbol'),
buy: 'bought',
}
this.bus.trigger('boughtOrSold', tradeItem);
order.destroy();
quote.buy();
}

else if (order.get('buy') == 'sell' && order.get('targetPrice') <= quote.get('price')) {
let tradeItem = {
price: order.get('targetPrice'),
symbol: order.get('symbol'),
buy: 'sold',
}
this.bus.trigger('boughtOrSold', tradeItem)
order.destroy();
quote.sell();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the workflow for executing an open order looks like this:

  1. Quote price changes
  2. quote_price_change event on the bus
  3. Handled here by OrderListView.buySellQuote()
  4. Find Orders matching the quote
  5. For each one, determine if it needs to be sold

But we can simplify this! The key observation is that when you create the Order you must know which Quote it's for - you need this information to validate the Order. Instead of working through the intermediaries of the bus and the OrderListView, each Order could listen directly to its Quote for price changes. Then the workflow would look like:

  1. Quote price changes
  2. Event on the quote
  3. Handler in Order
  4. Determine if the order needs to be sold

This eliminates dependencies on the bus and the OrderListView. It also simplifies the code, since we don't need to work with a list of Orders. If there are multiple Orders corresponding to the same quote each will have a separate handler registered, and each will run when the event occurs (steps 3-4).

}
});
}
});

export default OrderListView;
26 changes: 26 additions & 0 deletions src/views/order_view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Backbone from 'backbone';
import Order from '../models/order';

const OrderView = Backbone.View.extend({
initialize(params) {
this.template = params.template;
},

render() {
const compiledTemplate = this.template(this.model.toJSON());
this.$el.html(compiledTemplate);

return this;
},

events: {
'click button.btn-cancel': 'cancel',
},

cancel(event) {
this.model.destroy();
},

});

export default OrderView;
Loading