diff --git a/dist/index.html b/dist/index.html index 8a046fa..f09d0fc 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,45 +1,46 @@ - - Ada Trader - - - - - - -
-
-

Ada Trader

-
- -
- -
- -
-

Quotes

-
-
    -
-
+ + Ada Trader + + + + + + +
+
+

Ada Trader

+
+ +
+ +
+ +
+

Quotes

+
+
    +
-
-
+
+ +
-
-

Trade History

-
-
    -
-
+
+

Trade History

+
+
    +
-
+
+
+

@@ -53,13 +54,12 @@

Open Orders

-

Order Entry Form

- @@ -74,47 +74,49 @@

Order Entry Form

-
-
-
-
- + +
-
- - - - - - +
+ + + + + + - - + + diff --git a/spec/models/order_spec.js b/spec/models/order_spec.js new file mode 100644 index 0000000..f930f1d --- /dev/null +++ b/spec/models/order_spec.js @@ -0,0 +1,125 @@ +import Backbone from 'backbone'; +import _ from 'underscore'; + +import Quote from 'models/quote'; +import Order from 'models/order'; + +describe('model validations', () => { + let quote; + let buyOrder; + let sellOrder; + beforeEach(() => { + quote = new Quote({ + symbol: 'TESTQUOTE', + price: 100.00, + }); + buyOrder = new Order({ + symbol: 'TESTQUOTE', + targetPrice: 90.00, + buy: true, + activeQuote: quote, + symbolList: ['TESTQUOTE'] + }); + sellOrder = new Order({ + symbol: 'TESTQUOTE', + targetPrice: 101.00, + buy: false, + activeQuote: quote, + symbolList: ['TESTQUOTE'] + }); + }); + + it('valid sell order will work', () => { + expect(sellOrder.isValid()).toEqual(true); + }); + + it('valid buy order will work', () => { + expect(buyOrder.isValid()).toEqual(true); + }); + + + it('price cannot be blank', () => { + buyOrder.set('targetPrice', ''); + expect(buyOrder.isValid()).toEqual(false); + }); + + it('price cannot be a negative number', () => { + buyOrder.set('targetPrice', -1); + expect(buyOrder.isValid()).toEqual(false); + }); + + it('symbol cannot be blank', () => { + buyOrder.set('symbol', ''); + expect(buyOrder.isValid()).toEqual(false); + }); + + it('buy price cannot be equal to current price', () => { + buyOrder.set('targetPrice', 100.00); + expect(buyOrder.isValid()).toEqual(false); + }); + + it('buy price cannot be higher than current price', () => { + buyOrder.set('targetPrice', 101.00); + expect(buyOrder.isValid()).toEqual(false); + }); + + + it('sell price cannot be lower than current price', () => { + sellOrder.set('targetPrice', 100.00); + expect(sellOrder.isValid()).toEqual(false); + }); + + it('sell price cannot be equal to current price', () => { + sellOrder.set('targetPrice', 100.00); + expect(sellOrder.isValid()).toEqual(false); + }); +}); + +describe('model custom methods', () => { + let testOrder1; + let testOrder2; + let bus; + beforeEach(() => { + bus = {}; + bus = _.extend(bus, Backbone.Events); + + const quote = new Quote({ + symbol: 'HELLO', + price: 100.00, + }); + + const buyOrderData = { + symbol: 'HELLO', + targetPrice: 99.00, + buy: true, + bus: bus, + activeQuote: quote, + symbolList: ['HELLO'] + } + + const sellOrderData = { + symbol: 'HELLO', + targetPrice: 99.00, + buy: false, + bus: bus, + activeQuote: quote, + symbolList: ['HELLO'] + } + + testOrder1 = new Order(buyOrderData); + testOrder2 = new Order(sellOrderData); +}); + + it('valid buy order triggers bus', () => { + const spy = spyOn(bus, 'trigger'); + testOrder1.quote.set('price', 50.00); + expect(spy).toHaveBeenCalled(); + }); + + it('valid sell order triggers bus', () => { + const spy = spyOn(bus, 'trigger'); + testOrder2.quote.set('price', 101.00); + expect(spy).toHaveBeenCalled(); + }); + +}); diff --git a/src/app.js b/src/app.js index 03ec910..114c357 100644 --- a/src/app.js +++ b/src/app.js @@ -2,9 +2,22 @@ import 'foundation-sites/dist/foundation.css'; import 'css/app.css'; import $ from 'jquery'; - +import _ from 'underscore'; import Simulator from 'models/simulator'; + +import Quote from 'models/quote'; +import QuoteView from 'views/quote_view'; import QuoteList from 'collections/quote_list'; +import QuoteListView from 'views/quote_list_view'; + +import Order from 'models/order'; +import OrderList from 'collections/order_list'; +import OrderView from 'views/order_view'; +import OrderListView from 'views/order_list_view'; + +let bus = {}; +bus = _.extend(bus, Backbone.Events); +const quoteList = new QuoteList(); const quoteData = [ { @@ -32,4 +45,21 @@ $(document).ready(function() { }); simulator.start(); + + const quoteListView = new QuoteListView({ + model: quotes, + template: _.template($('#quote-template').html()), + el: '#quotes-container', + bus: bus + }); + quoteListView.render(); + + const orders = new OrderList(); + const orderListView = new OrderListView({ + model: orders, + symbols: quotes, + template: _.template($('#order-template').html()), + el: '#order-workspace', + bus: bus + }); }); 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/models/order.js b/src/models/order.js new file mode 100644 index 0000000..9adc924 --- /dev/null +++ b/src/models/order.js @@ -0,0 +1,57 @@ +import Backbone from 'backbone'; +import QuoteList from 'collections/quote_list'; + +const Order = Backbone.Model.extend({ + defaults: { + symbol: '', + targetPrice: '', + buy: '', + }, + initialize(attributes) { + this.buy = attributes.buy; + this.targetPrice = attributes.targetPrice + this.quote = attributes.activeQuote; + this.symbolList = attributes.symbolList; + this.listenTo(this.quote, 'change', this.quoteCheck); + this.bus = attributes.bus; + }, + validate(attributes) { + const errors = {}; + + if (!attributes.targetPrice) { + errors['targetPrice'] = ['Price is cannot be blank']; + } + + if (attributes.targetPrice <= 0) { + errors['targetPrice'] = ['Price can not be a negative number'] + } + + if (!attributes.symbol) { + errors['symbol'] = ['Symbol can not be blank']; + } + + if (attributes.buy && parseFloat(attributes.targetPrice) >= parseFloat(attributes.activeQuote.get('price'))) { + errors['targetPrice'] = ['Buy order price too high'] + } + + if (!attributes.buy && parseFloat(attributes.targetPrice) <= parseFloat(attributes.activeQuote.get('price'))) { + errors['targetPrice'] = ['Sell order price too low'] + } + + if ( Object.keys(errors).length > 0 ) { + return errors; + } else { + return false; + } + }, + + quoteCheck: function(e) { + if (this.buy && parseFloat(this.targetPrice) > parseFloat(this.quote.get('price'))) { + this.bus.trigger(`buyMe${this.quote.attributes.symbol}`, this.quote.attributes); + } else if (!this.buy && parseFloat(this.targetPrice) < parseFloat(this.quote.get('price'))) { + this.bus.trigger(`sellMe${this.quote.attributes.symbol}`, this.quote.attributes); + } + }, +}); + +export default Order; diff --git a/src/models/quote.js b/src/models/quote.js index 4fbf466..bdd2ac0 100644 --- a/src/models/quote.js +++ b/src/models/quote.js @@ -7,11 +7,16 @@ const Quote = Backbone.Model.extend({ }, buy() { - // Implement this function to increase the price by $1.00 + const current_price = this.get('price'); + this.set('price', current_price + 1.00); + console.log(`BUYING ${this.get('symbol')} at ${this.get('price')}`); }, sell() { - // Implement this function to decrease the price by $1.00 + const current_price = this.get('price'); + this.set('price', current_price - 1.00); + console.log(`SELLING ${this.get('symbol')} at ${this.get('price')}`); + }, }); diff --git a/src/views/order_list_view.js b/src/views/order_list_view.js new file mode 100644 index 0000000..afe063b --- /dev/null +++ b/src/views/order_list_view.js @@ -0,0 +1,91 @@ +import Backbone from 'backbone'; +import _ from 'underscore'; +import $ from 'jquery'; + +import Order from '../models/order'; +import OrderView from '../views/order_view'; + +import Quote from '../models/quote'; +import QuoteList from '../collections/quote_list'; + +const OrderListView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.quoteList = params.symbols + this.bus = params.bus; + this.listenTo(this.model, 'update', this.render); + this.orderForm(params.symbols.models); + }, + + 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; + }, + orderForm(symbol_names) { + symbol_names.forEach( (symbol_name) => { + $('select').append(``) + }); + }, + events: { + 'click button.btn-buy': 'buyOrder', + 'click button.btn-sell': 'sellOrder' + }, + buyOrder: function(event) { + event.preventDefault(); + const orderData = {}; + orderData['symbol'] = this.$(`[name=symbol]`).val(); + orderData['targetPrice'] = parseFloat(this.$(`[name=price-target]`).val()); + orderData['buy'] = true; + const searchElem = this.quoteList.findWhere({symbol: orderData['symbol']}); + orderData['activeQuote'] = searchElem; + orderData['bus'] = this.bus; + orderData['symbolList'] = this.quoteList.map(quote => quote.get('symbol')); + const newOrder = new Order(orderData); + if (newOrder.isValid()) { + this.model.add(newOrder); + } else { + newOrder.destroy(); + this.showMessage(newOrder.validationError); + } + }, + sellOrder: function(event) { + event.preventDefault(); + const orderData = {}; + orderData['symbol'] = this.$(`[name=symbol]`).val(); + orderData['targetPrice'] = parseInt(this.$(`[name=price-target]`).val()); + orderData['buy'] = false; + orderData['bus'] = this.bus; + const searchElem = this.quoteList.findWhere({symbol: orderData['symbol']}) + orderData['activeQuote'] = searchElem + orderData['symbolList'] = this.quoteList.map(quote => quote.get('symbol')); + const newOrder = new Order(orderData); + if (newOrder.isValid()) { + this.model.add(newOrder); + } else { + this.showMessage(newOrder.validationError); + } + }, + showMessage: function(messageObj) { + const messageArea = this.$('.form-errors'); + messageArea.empty(); + _.each(messageObj, (messages) => { + messages.forEach((text) => { + messageArea.append(`

${text}

`); + }) + }); + messageArea.show(); + }, + +}); + +export default OrderListView; diff --git a/src/views/order_view.js b/src/views/order_view.js new file mode 100644 index 0000000..8ed453b --- /dev/null +++ b/src/views/order_view.js @@ -0,0 +1,28 @@ +import Backbone from 'backbone'; +import _ from 'underscore'; + +import Quote from '../models/quote'; +import QuoteList from '../collections/quote_list'; + +const OrderView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.listenTo(this.model, 'change', this.render); + this.bus = params.bus; + this.listenTo(this.bus, `${this.model.attributes.symbol}${this.model.attributes.buy}orderFinished`, this.cancelOrder); + }, + render() { + const template = this.template(this.model.toJSON()); + this.$el.html(template); + return this; + }, + events: { + 'click button.btn-cancel': 'cancelOrder' + }, + cancelOrder: function(e) { + this.model.destroy(); + this.remove(); + } +}); + +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..2713db5 --- /dev/null +++ b/src/views/quote_list_view.js @@ -0,0 +1,27 @@ +import Backbone from 'backbone'; +import _ from 'underscore'; +import QuoteView from '../views/quote_view'; +import Quote from '../models/quote'; + +const QuoteListView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.bus = params.bus; + this.listenTo(this.model, 'update', this.render); + }, + render() { + 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); + }); + return this; + }, +}); + +export default QuoteListView; diff --git a/src/views/quote_view.js b/src/views/quote_view.js new file mode 100644 index 0000000..5088a6c --- /dev/null +++ b/src/views/quote_view.js @@ -0,0 +1,43 @@ +import $ from 'jquery'; +import _ from 'underscore'; + +import Backbone from 'backbone'; +import Quote from '../models/quote'; + +const tradeTemplate = _.template($('#trade-template').html()); + +const QuoteView = Backbone.View.extend({ + initialize(params) { + this.template = params.template; + this.listenTo(this.model, 'change', this.render); + this.bus = params.bus; + this.listenTo(this.bus, `buyMe${this.model.get('symbol')}`, this.buyQuote); + this.listenTo(this.bus, `sellMe${this.model.get('symbol')}`, this.sellQuote); + }, + render() { + const template = this.template(this.model.toJSON()); + this.$el.html(template); + return this; + }, + events: { + 'click button.btn-buy': 'buyQuote', + 'click button.btn-sell': 'sellQuote' + }, + buyQuote: function() { + const quote = this.model.attributes; + quote['buy'] = true; + this.model.buy(); + $('#trades').prepend(tradeTemplate(quote)); + this.bus.trigger(`${this.model.attributes.symbol}${this.model.attributes.buy}orderFinished`, this); + }, + sellQuote: function(e) { + const trade = this.model.attributes; + trade['buy'] = false; + this.model.sell(); + $('#trades').prepend(tradeTemplate(trade)); + this.bus.trigger(`${this.model.attributes.symbol}${this.model.attributes.buy}orderFinished`, this); + }, + +}); + +export default QuoteView;