Ada Trader
-
-
-
-
-
- Ada Trader
+
+
+
+
+
+
+
+
+
+
+
Quotes
-
-
+
+ -
-
Ada Trader
+
+
+
-
- -
+
+
+
+
+
- Quotes
+
+
-
-
+
+
-
-
+ Trade History
-
-
- -
-
+
+
+
+
Trade History
+
+
-
+
-
- -
-
-
-
-
-
-
- Open Orders
-
-
- -
-
-
+
-
-
- Order Entry Form
- -
-
-
+
+
+
+
+
+
+
-
-
+
+
diff --git a/spec/models/order_spec.js b/spec/models/order_spec.js
new file mode 100644
index 0000000..9cc8e8d
--- /dev/null
+++ b/spec/models/order_spec.js
@@ -0,0 +1,89 @@
+import Order from 'models/order';
+
+describe('Order spec', () => {
+ let buyOrder;
+ let sellOrder;
+ beforeEach(() => {
+ buyOrder = new Order({
+ symbol: 'HELLO',
+ targetPrice: 90.00,
+ marketPrice: 100.00,
+ buy: true
+ });
+ sellOrder = new Order({
+ symbol: 'HELLO',
+ targetPrice: 100.00,
+ marketPrice: 90.00,
+ buy: false
+ })
+ });
+
+ describe('Order validations', () => {
+ it('is not valid unless it has a symbol', () => {
+
+ expect(buyOrder.isValid()).toBeTruthy();
+
+ expect(sellOrder.isValid()).toBeTruthy();
+
+ buyOrder.set('symbol', null);
+
+ expect(buyOrder.isValid()).toBeFalsy();
+
+ sellOrder.set('symbol', null);
+
+ expect(sellOrder.isValid()).toBeFalsy();
+ })
+
+ it('is not valid unless it has a numerical targetPrice', () => {
+ expect(buyOrder.isValid()).toBeTruthy();
+
+ expect(sellOrder.isValid()).toBeTruthy();
+
+ const badTargetPrices = [null, 'strings', '.', '-1']
+
+ badTargetPrices.forEach(function(element) {
+
+ buyOrder.set('targetPrice', element);
+
+ sellOrder.set('targetPrice', element);
+
+ expect(buyOrder.isValid()).toBeFalsy();
+
+ expect(sellOrder.isValid()).toBeFalsy();
+ })
+ })
+
+ it('must follow buy low, sell high', () => {
+ expect(buyOrder.isValid()).toBeTruthy();
+
+ expect(sellOrder.isValid()).toBeTruthy();
+
+ buyOrder.set('targetPrice', '110');
+
+ sellOrder.set('targetPrice', '80');
+
+ expect(buyOrder.isValid()).toBeFalsy();
+
+ expect(sellOrder.isValid()).toBeFalsy();
+
+ })
+ });
+
+ describe('Execute order', () => {
+ it('triggers a "buy" event on itself if new buyOrder is valid', () => {
+ const changeInfo = {symbol: 'HELLO', buy: true, currentPrice: 80}
+ spyOn(buyOrder, "trigger")
+ buyOrder.executeOrder(changeInfo)
+
+ expect(buyOrder.trigger).toHaveBeenCalledWith('buy', changeInfo);
+ });
+
+ it('triggers a "sell" event on itself if new sellOrder is valid', () => {
+ const changeInfo = {symbol: 'HELLO', buy: false, currentPrice: 110}
+ spyOn(sellOrder, "trigger")
+ sellOrder.executeOrder(changeInfo)
+
+ expect(sellOrder.trigger).toHaveBeenCalledWith('sell', changeInfo);
+ });
+ });
+});
diff --git a/src/app.js b/src/app.js
index 03ec910..ecf0520 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1,11 +1,22 @@
+// CSS
import 'foundation-sites/dist/foundation.css';
import 'css/app.css';
+// Vendor Modules
import $ from 'jquery';
+import _ from 'underscore';
-import Simulator from 'models/simulator';
-import QuoteList from 'collections/quote_list';
+// Models
+import Simulator from './models/simulator';
+import QuoteList from './collections/quote_list';
+import OrderList from './collections/order_list';
+import Bus from './models/event_bus';
+// Views
+import QuoteListView from './views/quote_list_view';
+import OrderListView from './views/order_list_view';
+
+// Data
const quoteData = [
{
symbol: 'HUMOR',
@@ -25,11 +36,37 @@ const quoteData = [
},
];
+
$(document).ready(function() {
- const quotes = new QuoteList(quoteData);
+
+ const quoteList = new QuoteList(quoteData);
+
+ const orderList = new OrderList;
+
const simulator = new Simulator({
- quotes: quotes,
+ quotes: quoteList,
});
+ const bus = new Bus;
+
simulator.start();
+
+ const quoteListView = new QuoteListView({
+ model: quoteList,
+ template: _.template($('#quote-template').html()),
+ tradeTemplate: _.template($('#trade-template').html()),
+ el: 'main',
+ orderList: orderList,
+ bus: bus
+ });
+
+ const orderListView = new OrderListView({
+ model: orderList,
+ template: _.template($('#order-template').html()),
+ el: 'main',
+ quoteList: quoteList,
+ bus: bus
+ });
+
+ quoteListView.render();
});
diff --git a/src/collections/order_list.js b/src/collections/order_list.js
new file mode 100644
index 0000000..6b77957
--- /dev/null
+++ b/src/collections/order_list.js
@@ -0,0 +1,8 @@
+import Backbone from 'backbone';
+import Order from '../models/order';
+
+const OrderList = Backbone.Collection.extend({
+ model: Order,
+});
+
+export default OrderList;
diff --git a/src/collections/quote_list.js b/src/collections/quote_list.js
index 8da08cb..bfa112d 100644
--- a/src/collections/quote_list.js
+++ b/src/collections/quote_list.js
@@ -1,5 +1,5 @@
import Backbone from 'backbone';
-import Quote from 'models/quote';
+import Quote from '../models/quote';
const QuoteList = Backbone.Collection.extend({
model: Quote,
diff --git a/src/models/event_bus.js b/src/models/event_bus.js
new file mode 100644
index 0000000..b95a159
--- /dev/null
+++ b/src/models/event_bus.js
@@ -0,0 +1,7 @@
+import Backbone from 'backbone';
+
+const Bus = Backbone.Model.extend({
+
+});
+
+export default Bus;
diff --git a/src/models/order.js b/src/models/order.js
new file mode 100644
index 0000000..00f542e
--- /dev/null
+++ b/src/models/order.js
@@ -0,0 +1,35 @@
+import Backbone from 'backbone';
+
+const Order = Backbone.Model.extend({
+ validate: function(attributes) {
+ const errors = {};
+ if (!attributes.targetPrice) {
+ errors['Price'] = ['cannot be blank'];
+ } else if (isNaN(attributes.targetPrice)) {
+ errors['Price'] = ['must be a number - leave off $'];
+ } else if (attributes.targetPrice <= 0){
+ errors['Price'] = ['must be greater than 0']
+ } else if (attributes.buy && attributes.targetPrice > attributes.marketPrice) {
+ errors['Price'] = ['cannot be higher than market price for buy orders'];
+ } else if (!attributes.buy && attributes.targetPrice < attributes.marketPrice) {
+ errors['Price'] = ['cannot be lower than market price for sell orders'];
+ } else if (!attributes.symbol) {
+ errors['Symbol'] = ['cannot be blank'];
+ }
+ if (Object.keys(errors).length > 0) {
+ return errors;
+ } else {
+ return false;
+ }
+ },
+ executeOrder: function(changeInfo) {
+ if (this.get('symbol') === changeInfo.symbol && this.get('buy') === true && changeInfo.currentPrice <= this.get('targetPrice')) {
+ this.trigger('buy', changeInfo);
+ }
+ if (this.get('symbol') === changeInfo.symbol && this.get('buy') === false && changeInfo.currentPrice >= this.get('targetPrice')) {
+ this.trigger('sell', changeInfo);
+ }
+ }
+});
+
+export default Order;
diff --git a/src/models/quote.js b/src/models/quote.js
index 4fbf466..63be309 100644
--- a/src/models/quote.js
+++ b/src/models/quote.js
@@ -7,11 +7,11 @@ const Quote = Backbone.Model.extend({
},
buy() {
- // Implement this function to increase the price by $1.00
+ return this.set('price', (this.get('price') + 1.00));
},
sell() {
- // Implement this function to decrease the price by $1.00
+ return this.set('price', (this.get('price') - 1.00));
},
});
diff --git a/src/views/order_list_view.js b/src/views/order_list_view.js
new file mode 100644
index 0000000..97c52a4
--- /dev/null
+++ b/src/views/order_list_view.js
@@ -0,0 +1,55 @@
+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.listenTo(this.model, 'update', this.render);
+ this.quoteListView = params.quoteListView;
+ this.bus = params.bus;
+ this.quoteList = params.quoteList;
+ },
+ render() {
+ this.$('#orders').empty();
+ this.model.each((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 .btn-buy': 'addOrder',
+ 'click .btn-sell': 'addOrder'
+ },
+ addOrder: function(event) {
+ event.preventDefault();
+ const orderData = this.$(event.target).attr('class').includes('btn-buy') ? { buy: true } : {buy: false}
+ orderData['symbol'] = this.$('select :selected').text();
+ const stringTargetPrice = this.$(`input[name=price-target]`).val();
+ orderData['targetPrice'] = parseFloat(stringTargetPrice);
+ orderData['marketPrice'] = this.quoteList.where({symbol: orderData['symbol']})[0].attributes.price;
+ const newOrder = new Order(orderData);
+ if (newOrder.isValid()) {
+ this.model.add(newOrder);
+ newOrder.listenTo(this.bus, 'priceChange', newOrder.executeOrder);
+ this.$('.order-entry-form [name=price-target]').val("");
+ this.$('.form-errors').empty();
+ } else {
+ this.$('.form-errors').empty();
+ for(let key in newOrder.validationError) {
+ newOrder.validationError[key].forEach((error) => {
+ this.$('.form-errors').append(`
+
-
-
-
-
-
-
-
-
+
+
Open Orders
+
+
+ -
+
${key}: ${error}
`); + }) + } + } + }, +}) + +export default OrderListView diff --git a/src/views/order_view.js b/src/views/order_view.js new file mode 100644 index 0000000..27926df --- /dev/null +++ b/src/views/order_view.js @@ -0,0 +1,30 @@ +import Backbone from 'backbone'; + +const OrderView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.bus = params.bus; + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model, 'buy', this.triggerBuy); + this.listenTo(this.model, 'sell', this.triggerSell); + }, + render() { + const compiledTemplate = this.template(this.model.toJSON()); + this.$el.html(compiledTemplate); + return this; + }, + events: { + 'click button.btn-cancel': 'deleteOrder', + }, + deleteOrder: function() { + this.model.destroy(); + }, + triggerBuy: function(changeInfo) { + this.bus.trigger('buyOrder', {quote: changeInfo, model: this.model}); + }, + triggerSell: function(changeInfo) { + this.bus.trigger('sellOrder', {quote: changeInfo, model: this.model}); + } +}) + +export default OrderView; diff --git a/src/views/quote_list_view.js b/src/views/quote_list_view.js new file mode 100644 index 0000000..f0527bf --- /dev/null +++ b/src/views/quote_list_view.js @@ -0,0 +1,50 @@ +import Backbone from 'backbone'; +import QuoteView from './quote_view'; +//import Quote from '../models/quote'; + +const QuoteListView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.tradeTemplate = params.tradeTemplate; + this.orderList = params.orderList; + this.listenTo(this.model, 'update', this.render); + this.bus = params.bus; + }, + render() { + this.$('#quotes').empty(); + this.$('select').empty(); + this.model.each((quote) => { + const quoteView = new QuoteView({ + model: quote, + template: this.template, + tradeTemplate: this.tradeTemplate, + bus: this.bus, + tagName: 'li', + className: 'quote', + }); + this.listenTo(quoteView, 'buy', this.displayBuy); + + this.listenTo(quoteView, 'sell', this.displaySell); + + this.listenTo(quoteView, 'priceChange', this.alertPriceChange); + + this.$('#quotes').append(quoteView.render().$el); + + this.$('select').append(``) + + + }); + return this; + }, + displayBuy(quoteView) { + this.$('#trades').prepend(this.tradeTemplate({buy: true, symbol: quoteView.model.get('symbol'), price: quoteView.model.get('price')})) + }, + displaySell(quoteView) { + this.$('#trades').prepend(this.tradeTemplate({buy: false, symbol: quoteView.model.get('symbol'), price: quoteView.model.get('price')})) + }, + alertPriceChange(changeInfo) { + this.trigger('priceChange', changeInfo); + } +}); + +export default QuoteListView; diff --git a/src/views/quote_view.js b/src/views/quote_view.js new file mode 100644 index 0000000..264c975 --- /dev/null +++ b/src/views/quote_view.js @@ -0,0 +1,49 @@ +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); + this.listenTo(this.model, 'change', this.alertPriceChange); + this.listenTo(this.bus, 'buyOrder', this.buyOrder); + this.listenTo(this.bus, 'sellOrder', this.sellOrder); + }, + render() { + const compiledTemplate = this.template(this.model.toJSON()); + this.$el.html(compiledTemplate); + return this; + }, + alertPriceChange() { + this.bus.trigger('priceChange', {symbol: this.model.get('symbol'), currentPrice: this.model.get('price')}); + }, + events: { + 'click button.btn-buy': 'buyQuote', + 'click button.btn-sell': 'sellQuote' + }, + buyQuote: function() { + this.trigger('buy', this); + this.model.buy(); + }, + sellQuote: function() { + this.trigger('sell', this); + this.model.sell(); + }, + buyOrder: function(changeInfo) { + if (this.model.get(`symbol`) === changeInfo.quote.symbol && this.model.get('price') <= changeInfo.quote.currentPrice) { + this.trigger('buy', this); + this.model.buy(); + changeInfo.model.destroy(); + } + }, + sellOrder: function(changeInfo) { + if (this.model.get(`symbol`) === changeInfo.quote.symbol && this.model.get('price') >= changeInfo.quote.currentPrice) { + this.trigger('sell', this); + this.model.sell(); + changeInfo.model.destroy(); + } + } +}) + +export default QuoteView;