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 - Rebecca - Ada Trader #34

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions spec/models/open_order_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import OpenOrder from 'models/open_order';
import Quote from 'models/quote';


describe('OpenOrder spec', () => {
let currentQuote;
let OrderAtQuotePrice;
let OrderAboveQuotePrice;
let OrderBelowQuotePrice;

beforeEach(() => {
currentQuote = new Quote({
symbol: 'HUMOR',
price: 85.00,
})

OrderAtQuotePrice = new OpenOrder({
targetPrice: 85.0,
symbol: 'HUMOR',
quote: currentQuote,
buy: true,
})

OrderAboveQuotePrice = new OpenOrder({
targetPrice: 99.0,
symbol: 'HUMOR',
quote: currentQuote,
buy: true,
})

OrderBelowQuotePrice = new OpenOrder({
targetPrice: 83.0,
symbol: 'HUMOR',
quote: currentQuote,
buy: true,
})
});

describe('validate', () => {
it ('does not allow for price to be blank', () => {

const invalidOrder = new OpenOrder({
targetPrice: '',
symbol: 'HUMOR',
quote: currentQuote,
buy: true,
})

expect(invalidOrder.isValid()).toBeFalsy();
});

// buy order validations
it ('does not allow for buy orders where target price is equal to the quote', () => {

expect(OrderAtQuotePrice.isValid()).toBeFalsy();
});

it ('does not allow for buy orders where target price is greater than the quote', () => {

expect(OrderAboveQuotePrice.isValid()).toBeFalsy();
});

it ('allows for buy orders where target price is less than the quote', () => {

expect(OrderBelowQuotePrice.isValid()).toBeTruthy(`error: ${OrderBelowQuotePrice.validationError}`);
});

// sell order validations
it ('does not allow for sell orders where target price is equal to the quote', () => {

OrderAtQuotePrice.set('buy', false);

expect(OrderAtQuotePrice.isValid()).toBeFalsy();
});

it ('does not allow for sell orders where target price is less than the quote', () => {

OrderBelowQuotePrice.set('buy', false);

expect(OrderBelowQuotePrice.isValid()).toBeFalsy();
});

it ('allows for sell orders where target price is greater than the quote', () => {

OrderAboveQuotePrice.set('buy', false)

expect(OrderAboveQuotePrice.isValid()).toBeTruthy(`error: ${OrderAboveQuotePrice.validationError}`);
});
});

});
51 changes: 51 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -2,9 +2,20 @@ import 'foundation-sites/dist/foundation.css';
import 'css/app.css';

import $ from 'jquery';
import _ from 'underscore';
import Backbone from 'backbone';

import Simulator from 'models/simulator';
import QuoteList from 'collections/quote_list';
import OpenOrderList from 'collections/open_order_list';

import Quote from './models/quote';
import QuoteView from './views/quote_view';
import QuoteListView from './views/quote_list_view';
import TradeListView from './views/trade_list_view';
import OpenOrder from './models/open_order';
import OpenOrderView from './views/open_order_view';
import OpenOrderListView from './views/open_order_list_view';

const quoteData = [
{
@@ -25,11 +36,51 @@ const quoteData = [
},
];

const openOrderList = new OpenOrderList();

let quoteTemplate;
let tradeTemplate;
let orderTemplate;

$(document).ready(function() {
let bus = {};

bus = _.extend(bus, Backbone.Events);
quoteTemplate = _.template($('#quote-template').html());
orderTemplate = _.template($('#order-template').html());


tradeTemplate = _.template($('#trade-template').html());

const quotes = new QuoteList(quoteData);
const simulator = new Simulator({
quotes: quotes,
});

// Quotelistview will encompass the main tag
const quoteListView = new QuoteListView({
el: 'main',
model: quotes,
template: quoteTemplate,
bus: bus,
});

// TradelistView will encompass the main tag
const tradelistView = new TradeListView({
el: '#trades-container',
bus: bus,
template: tradeTemplate,
});

// OpenOrderListview will encompass the orderWorkspace div
const openOrderListView = new OpenOrderListView({
el: '#order-workspace',
model: openOrderList,
template: orderTemplate,
bus: bus,
});

quoteListView.render();

simulator.start();
});
8 changes: 8 additions & 0 deletions src/collections/open_order_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Backbone from 'backbone';
import OpenOrder from 'models/open_order';

const OpenOrderList = Backbone.Collection.extend({
model: OpenOrder,
});

export default OpenOrderList;
7 changes: 7 additions & 0 deletions src/collections/quote_list.js
Original file line number Diff line number Diff line change
@@ -3,6 +3,13 @@ import Quote from 'models/quote';

const QuoteList = Backbone.Collection.extend({
model: Quote,
symbols() {
let symbols = [];
this.each((quote) => {
symbols.push(quote.get('symbol'));
});
return symbols;
}
});

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

const OpenOrder = Backbone.Model.extend({
initialize(attributes) {
},
validate(attributes) {
const errors = {};

if (!attributes.targetPrice) {
errors['target_price'] = ["Price cannot be blank."];
}

if (attributes.buy && attributes.targetPrice >= attributes.quote.get('price')) {
errors['target_price'] = ["The price you listed for a buy order is greater than or equal to the current quote price."];
}

if (!attributes.buy && attributes.targetPrice <= attributes.quote.get('price')) {
errors['target_price'] = ["The price you listed for a sell order is less than or equal to the current quote price."];
}

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

export default OpenOrder;
6 changes: 4 additions & 2 deletions src/models/quote.js
Original file line number Diff line number Diff line change
@@ -7,11 +7,13 @@ const Quote = Backbone.Model.extend({
},

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

sell() {
// Implement this function to decrease the price by $1.00
let newPrice = this.get('price') - 1;
this.set({price: newPrice});
},
});

167 changes: 167 additions & 0 deletions src/views/open_order_list_view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import Backbone from 'backbone';
import OpenOrderView from '../views/open_order_view'

import OpenOrder from '../models/open_order';

const OpenOrderListView = Backbone.View.extend({
initialize(params) {
this.template = params.template;
this.bus = params.bus;
this.listenTo(this.model, 'update', this.render);
this.listenTo(this.bus, 'quote_symbols', this.symbolDropdown);
this.listenTo(this.bus, 'quote_change', this.checkOpenOrders);

// list for current quote list
this.listenTo(this.bus, 'current_quote_list', this.getQuoteList);
},
getQuoteList(quoteList) {
this.quoteList = quoteList;

Choose a reason for hiding this comment

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

Instead of using an event on the bus to get the quoteList, why not pass it in as an extra parameter when instantiating the OpenOrderListView? Building an event for it is overkill.

console.log('the quote list !!')
console.log(this.quoteList);

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

this.model.each((openOrder) => {
const openOrderView = new OpenOrderView({
model: openOrder,
template: this.template,
tagName: 'li',
className: 'order',
bus: this.bus,
});
// render returns the taskview (this) which allows you to append
this.$('#orders').append(openOrderView.render().$el);
});
return this;
},
checkOpenOrders(quote) {

let openOrders = this.model.where({symbol: quote.get('symbol')});

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_change event on the bus
  3. Handled here by OpenOrderListView.checkOpenOrders()
  4. Find OpenOrders matching the quote
  5. For each one, determine if it needs to be sold

But we can simplify this! The key observation is that each OpenOrder already knows which Quote it's for - we need this information to validate the OpenOrder. Instead of working through the intermediaries of the bus and the OpenOrderListView, each OpenOrder 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 OpenOrder
  4. Determine if the order needs to be sold

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

if (openOrders.length >= 1) {
openOrders.forEach((openOrder) => {

Choose a reason for hiding this comment

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

You don't need to wrap the call to forEach with a check that length >= 1. If the list has zero elements, then the loop body will execute zero times.

// if you are buying
if (openOrder.get('buy') && openOrder.get('targetPrice') >= quote.get('price')) {
let tradeObject = {
price: openOrder.get('targetPrice'),
symbol: openOrder.get('symbol'),
buy: true,
}
this.bus.trigger('add_trade', tradeObject)
// destroy purchased open order
openOrder.destroy();
// make sure quote is recognized as bought so price increases appropriately
quote.buy();
}
else if ((!openOrder.get('buy')) && openOrder.get('targetPrice') <= quote.get('price')) {
let tradeObject = {
price: openOrder.get('targetPrice'),
symbol: openOrder.get('symbol'),
buy: false,
}
this.bus.trigger('add_trade', tradeObject)
// destroy purchased open order
openOrder.destroy();
// make sure quote is recognized as sold so price decreases appropriately
quote.sell();
}
});
}
},
symbolDropdown(quotes) {
quotes.forEach((quote) => {
this.$('select[name=symbol]').append(`<option value="${quote}">${quote}</option>`)
});
},
events: {
'click button.btn-buy': 'addBuyOpenOrder',
'click button.btn-sell': 'addSellOpenOrder',
},
updateStatusMessageFrom(messageHash) {
const $formErrors = this.$('.form-errors');

$formErrors.empty();
Object.keys(messageHash).forEach((messageType) => {
messageHash[messageType].forEach((message) => {
$formErrors.append(`<li>${message}</li>`);
});
$formErrors.show();
});
},
// TODO: consolidate addBuyOpenOrder and addSellOpenOrder
addBuyOpenOrder(event) {
event.preventDefault();

let formData = this.getFormData();

formData['buy'] = true;
let correctQuote = this.quoteList.findWhere({symbol: formData['symbol']})
formData['quote'] = correctQuote;
const newOpenOrder = new OpenOrder(formData);
if (newOpenOrder.isValid()) {
this.model.add(newOpenOrder);
this.clearFormData();
console.log('in add buy open order');
} else {
console.log('ERROR');
this.updateStatusMessageFrom(newOpenOrder.validationError);
newOpenOrder.destroy();
}
},
addSellOpenOrder(event) {
event.preventDefault();

let formData = this.getFormData();

formData['buy'] = false;

let correctQuote = this.quoteList.findWhere({symbol: formData['symbol']})
formData['quote'] = correctQuote;

const newOpenOrder = new OpenOrder(formData);
// if is valid add newTask and clearformdata
if (newOpenOrder.isValid()) {
this.model.add(newOpenOrder);
this.clearFormData();
console.log('in add sell open order');
} else {
console.log('ERROR');
this.updateStatusMessageFrom(newOpenOrder.validationError);
newOpenOrder.destroy();
}
},

getFormData() {
console.log('in get form data');
const openOrderData = {};
['symbol', 'price-target'].forEach((field) => {
let val;
if (field === 'symbol') {
val =
this.$(`.order-entry-form select[name=${field}]`).val();
} else {
val =
parseFloat(this.$(`.order-entry-form input[name=${field}]`).val());
field = 'targetPrice';
}
if (val !== '') {
openOrderData[field] = val;
}
});

return openOrderData;
},
clearFormData () {
['symbol', 'price-target'].forEach((field) => {
if (field === 'symbol') {
this.$(`.order-entry-form select[name=${field}]`).val('');
} else {
this.$(`.order-entry-form input[name=${field}]`).val('');
}
});
},
});


export default OpenOrderListView;
27 changes: 27 additions & 0 deletions src/views/open_order_view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Backbone from 'backbone';
import OpenOrder from '../models/open_order';

const OpenOrderView = Backbone.View.extend({
initialize(params) {
this.template = params.template;
this.bus = params.bus;
this.listenTo(this.model, 'change', this.render);
},

render() {
const compiledTemplate = this.template(this.model.toJSON());
this.$el.html(compiledTemplate);
return this;
},
events: {
'click button.btn-cancel': 'deleteOpenOrder',
},

deleteOpenOrder(event) {
this.model.destroy();
this.remove();
},

});

export default OpenOrderView;
36 changes: 36 additions & 0 deletions src/views/quote_list_view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Backbone from 'backbone';
import QuoteView from '../views/quote_view'

import Quote from '../models/quote';

const QuoteListView = Backbone.View.extend({
initialize(params) {
this.template = params.template;
this.listenTo(this.model, 'update', this.render);
this.bus = params.bus;
},
render() {
this.$('#quotes').empty();
console.log('in quotelistview render');
console.log(this.model)
this.model.each((quote) => {
const quoteView = new QuoteView({
model: quote,
template: this.template,
tagName: 'li',
className: 'quote',
bus: this.bus,
});
this.$('#quotes').append(quoteView.render().$el);
// pass the quotelist to the open order view
this.bus.trigger('current_quote_list', this.model)

});
// symbols() is a custom function in the collection that returns an array of models' symbols
let quotes = this.model.symbols();
this.bus.trigger('quote_symbols', quotes);
return this;
},
});

export default QuoteListView;
47 changes: 47 additions & 0 deletions src/views/quote_view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Backbone from 'backbone';
import Quote from '../models/quote';

const QuoteView = Backbone.View.extend({

initialize(params) {
this.template = params.template;
this.bus = params.bus;
this.listenTo(this.model, 'change', this.render);
},
events: {

'click button.btn-buy': 'buyOrSellQuote',
'click button.btn-sell': 'buyOrSellQuote',
},
buyOrSellQuote(event) {

let tradeObject = {
price: this.model.get('price'),
symbol: this.model.get('symbol'),
}

if (event.currentTarget.innerHTML === 'Buy') {
tradeObject['buy'] = true;
console.log(tradeObject);

this.bus.trigger('add_trade', tradeObject)
this.model.buy();
} else {
tradeObject['buy'] = false;
this.bus.trigger('add_trade', tradeObject)
this.model.sell();
}
},
render() {
let quote = this.model;
// trigger quote_change event which openOrderlist view will listen for
this.bus.trigger('quote_change', quote);
console.log(this.model);
const compiledTemplate = this.template(this.model.toJSON());
this.$el.html(compiledTemplate);
return this;
},

});

export default QuoteView;
16 changes: 16 additions & 0 deletions src/views/trade_list_view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Backbone from 'backbone';
import Quote from '../models/quote';

const TradeListView = Backbone.View.extend({
initialize(params) {
this.template = params.template;
this.bus = params.bus;
this.listenTo(this.bus, 'add_trade', this.render);
},
render(tradeObject) {
const compiledTemplate= this.template(tradeObject)
this.$('#trades').prepend(compiledTemplate);
},
});

export default TradeListView;