Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'master' of github.com:TeamGamma/directedstudies

  • Loading branch information...
commit 5b60f52b9abedcd9721223940bc67b8403f44410 2 parents fd56334 + 24f13a6
@alimjiwa alimjiwa authored
View
5 sps/config.py
@@ -23,7 +23,7 @@ class ConfigObject():
'password': 'root',
}
DATABASE_ENGINE_ARGS = {
- 'echo': 'debug',
+ 'echo': False,
}
DATABASE_TABLE_ARGS = {
@@ -38,6 +38,9 @@ class ConfigObject():
DUMPLOG_DIR = '/tmp/'
+ # Interval between trigger checks in seconds
+ TRIGGER_INTERVAL = 3
+
# The global configuration object
config = ConfigObject()
View
23 sps/database/models.py
@@ -52,6 +52,9 @@ class Money(namedtuple('Money', 'dollars cents')):
>>> m = Money(3,30) * 4
>>> m.dollars, m.cents
(13, 20)
+
+ >>> Money(4,20) > Money(4,10) and Money(3,30) < Money(4,20)
+ True
"""
__slots__ = ()
@@ -161,7 +164,7 @@ class Transaction(InitMixin, ReprMixin, Base):
operation = Column(String(4), nullable=False)
quantity = Column(Integer, nullable=False)
stock_value = composite(Money, _stock_value_dollars, _stock_value_cents)
- committed = Column(Boolean, nullable=False)
+ committed = Column(Boolean, nullable=False, default=False)
# Auto-set timestamp when created
creation_time = Column(DateTime, default=func.now())
@@ -186,12 +189,24 @@ class SetTransaction(InitMixin, ReprMixin, Base):
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String(50), ForeignKey('users.username'), nullable=False)
user = relationship("User", backref=backref('set_transactions'))
+ operation = Column(String(3), nullable=False)
stock_symbol = Column(String(STOCK_SYMBOL_LENGTH), nullable=False)
- trigger_value = composite(Money, _trigger_value_dollars, _trigger_value_cents)
+
+ # Dollar value of stock to BUY when the trigger point is reached
amount = composite(Money, _amount_dollars, _amount_cents)
+
+ # The trigger value for the stock to execute the BUY or SELL at
+ trigger_value = composite(Money, _trigger_value_dollars, _trigger_value_cents)
+
+ # Quantity of stock to SELL when the trigger point is reached
quantity = Column(Integer, default=0, nullable=False)
- active = Column(Boolean, nullable=False)
- operation = Column(String(3), nullable=False)
+
+ # True if the trigger is currently running, False if waiting for a SET_TRIGGER command
+ # Note: the SetTransaction will be replaced with a Transaction when the actual transaction happens
+ active = Column(Boolean, nullable=False, default=False)
+
+ # Marker that signals trigger to stop when True
+ cancelled = Column(Boolean, nullable=False, default=False)
# Auto-set timestamp when created
creation_time = Column(DateTime, default=func.now())
View
153 sps/transactions/commands.py
@@ -7,8 +7,12 @@
from sps.transactions import xml
from sps.config import config
from datetime import datetime
+import eventlet
from os import path
+from logging import getLogger
+log = getLogger(__name__)
+
class CommandError(Exception):
"""
An exception that holds both a message for the user and the original cause
@@ -40,6 +44,9 @@ class NoBuyTransactionError(CommandError):
class NoSellTransactionError(CommandError):
user_message = 'No SELL transaction is pending'
+class NoTriggerError(CommandError):
+ user_message = 'No trigger is pending'
+
class ExpiredBuyTransactionError(CommandError):
user_message = 'BUY transaction has expired'
@@ -152,7 +159,7 @@ def run(self, username, stock_symbol, amount):
# Work out quantity of stock to buy, fail if not enough for one stock
amount = Money.from_string(amount)
- quantity = self.quantity(quote, amount)
+ quantity = amount_to_quantity(quote, amount)
if quantity == 0:
raise InsufficientFundsError()
@@ -167,17 +174,6 @@ def run(self, username, stock_symbol, amount):
return xml.QuoteResponse(quantity=quantity, price=price)
- def quantity(self, price, amount):
- q = 0
- while(True):
- amount = amount - price
- if (amount.dollars < 0) or (amount.cents < 0):
- break
- else:
- q += 1
-
- return q
-
class COMMIT_BUYCommand(CommandHandler):
"""
@@ -413,7 +409,23 @@ class CANCEL_SET_BUYCommand(CommandHandler):
Cancels a SET_BUY command issued for the given stock
"""
def run(self, username, stock_symbol):
- return xml.ResultResponse('success')
+ session = get_session()
+ user = session.query(User).filter_by(username=username).first()
+ if not user:
+ raise UserNotFoundError(username)
+
+ trigger = session.query(SetTransaction).filter_by(
+ username=user.username, operation='BUY', stock_symbol=stock_symbol,
+ cancelled=False
+ ).first()
+ if not trigger:
+ raise NoTriggerError(username, stock_symbol)
+
+ trigger.cancelled = True
+ session.commit()
+
+ return xml.ResultResponse('trigger cancelled')
+
class SET_BUY_TRIGGERCommand(CommandHandler):
@@ -422,7 +434,88 @@ class SET_BUY_TRIGGERCommand(CommandHandler):
will execute.
"""
def run(self, username, stock_symbol, amount):
- return xml.ResultResponse('success')
+ session = get_session()
+ user = session.query(User).filter_by(username=username).first()
+ if not user:
+ raise UserNotFoundError(username)
+
+ amount = Money.from_string(amount)
+
+ trigger = session.query(SetTransaction).filter_by(
+ username=user.username, operation='BUY', stock_symbol=stock_symbol,
+ active=False
+ ).first()
+ if not trigger:
+ raise NoTriggerError(username, stock_symbol)
+
+ trigger.active = True
+ trigger.trigger_value = amount
+ session.commit()
+
+ self.session = session
+ eventlet.spawn(self.check_trigger, trigger)
+
+ return xml.ResultResponse('trigger activated')
+
+ def check_trigger(self, trigger):
+ while True:
+ log.debug('Trigger %d checking for stock %s < %s',
+ trigger.id, trigger.stock_symbol, trigger.trigger_value)
+
+ # TODO: why is commit required here?
+ self.session.refresh(trigger)
+ self.session.commit()
+
+ if trigger.cancelled:
+ log.debug('Trigger %d cancelled!', trigger.id)
+ self.session.delete(trigger)
+ self.session.commit()
+ return
+
+ # Get a new quote for the stock
+ quote_client = get_quote_client()
+ quote = quote_client.get_quote(trigger.stock_symbol,
+ trigger.username)
+ log.debug('Trigger %d: %s => %s',
+ trigger.id, trigger.stock_symbol, quote)
+
+ # If quote is less than trigger value, buy stock and remove trigger
+ if quote < trigger.trigger_value:
+ # buy the stock and update reserve balance
+ log.debug("Trigger %d activated: %s < %s",
+ trigger.id, quote, trigger.trigger_value)
+
+ return self.process_transaction(quote, trigger)
+
+ eventlet.sleep(config.TRIGGER_INTERVAL)
+
+ def process_transaction(self, quote, trigger):
+ # Calculate real price of stock purchase based on current quote
+ user = trigger.user
+ quantity = amount_to_quantity(quote, trigger.amount)
+ real_amount = quote * quantity
+
+ log.debug('Buying %d units of %s (%s total) for %s',
+ quantity, trigger.stock_symbol, real_amount, user.username)
+
+ user.reserve_balance -= trigger.amount
+
+ # create or update the StockPurchase for this stock symbol
+ stock = self.session.query(StockPurchase).filter_by(
+ user=user, stock_symbol=trigger.stock_symbol
+ ).first()
+ if not stock:
+ log.debug('%s owns no %s yet, buying %d units...',
+ user.username, trigger.stock_symbol, quantity)
+ stock = StockPurchase(user=user,
+ stock_symbol=trigger.stock_symbol,
+ quantity=quantity)
+ else:
+ stock.quantity = stock.quantity + trigger.quantity
+
+ self.session.delete(trigger)
+
+ self.session.commit()
class SET_SELL_TRIGGERCommand(CommandHandler):
@@ -439,7 +532,23 @@ class CANCEL_SET_SELLCommand(CommandHandler):
Cancels the SET_SELL associated with the given stock and user
"""
def run(self, username, stock_symbol):
- return xml.ResultResponse('success')
+ session = get_session()
+ user = session.query(User).filter_by(username=username).first()
+ if not user:
+ raise UserNotFoundError(username)
+
+ trigger = session.query(SetTransaction).filter_by(
+ username=user.username, operation='SELL', stock_symbol=stock_symbol
+ ).first()
+ if not trigger:
+ raise NoTriggerError(username, stock_symbol)
+
+ session.delete(trigger)
+ session.commit()
+
+ return xml.ResultResponse('trigger cancelled')
+
+
class DUMPLOG_USERCommand(CommandHandler):
@@ -522,6 +631,20 @@ def dumplog_admin(self, filename):
return xml.ResultResponse('Wrote transactions to "%s"' % full_path)
+def amount_to_quantity(price, amount):
+ """ Given the price of a stock and an maximum dollar value to buy, returns
+ the quantity of stock that can be bought. """
+ q = 0
+ while(True):
+ amount = amount - price
+ if (amount.dollars < 0) or (amount.cents < 0):
+ break
+ else:
+ q += 1
+
+ return q
+
+
CommandHandler.register_command('ADD', ADDCommand)
CommandHandler.register_command('QUOTE', QUOTECommand)
CommandHandler.register_command('BUY', BUYCommand)
View
131 tests/test_commands.py
@@ -114,23 +114,23 @@ def test_postcondition_buy(self):
self.assertNotEqual(transaction, None)
def test_quantity_exact(self):
- quantity = self.command.quantity(Money(25, 60), Money(25, 60))
+ quantity = commands.amount_to_quantity(Money(25, 60), Money(25, 60))
self.assertEqual(quantity, 1)
def test_quantity_multiple(self):
- quantity = self.command.quantity(Money(25, 60), Money(102, 40))
+ quantity = commands.amount_to_quantity(Money(25, 60), Money(102, 40))
self.assertEqual(quantity, 4)
def test_quantity_less(self):
- quantity = self.command.quantity(Money(25, 60), Money(102, 30))
+ quantity = commands.amount_to_quantity(Money(25, 60), Money(102, 30))
self.assertEqual(quantity, 3)
def test_quantity_more(self):
- quantity = self.command.quantity(Money(25, 60), Money(102, 50))
+ quantity = commands.amount_to_quantity(Money(25, 60), Money(102, 50))
self.assertEqual(quantity, 4)
def test_quantity_less_than_one(self):
- quantity = self.command.quantity(Money(25, 60), Money(25, 40))
+ quantity = commands.amount_to_quantity(Money(25, 60), Money(25, 40))
self.assertEqual(quantity, 0)
@@ -249,6 +249,7 @@ class _TransactionCommandTest(object):
operation = None # 'BUY' or 'SELL'
missing_exception = None # e.g. NoBuyTransactionError
expired_exception = None # e.g. ExpiredBuyTransactionError
+ transaction_type = Transaction
def test_return_value(self):
""" Should return "success" """
@@ -272,7 +273,7 @@ def test_committed_transaction(self):
# Committed transaction record for user 1
self.add_all(
- Transaction(username='poor_user', stock_symbol='ABAB',
+ self.transaction_type(username='poor_user', stock_symbol='ABAB',
operation=self.operation, committed=True, quantity=1,
stock_value=Money(10, 54))
)
@@ -283,7 +284,7 @@ def test_expired_transaction_remove(self):
""" Should delete the transaction if user's transaction has expired """
# Expired transaction record for user 1
- trans = Transaction(username='poor_user', stock_symbol='ABAB',
+ trans = self.transaction_type(username='poor_user', stock_symbol='ABAB',
operation=self.operation, committed=False, quantity=1,
stock_value=Money(10, 54),
creation_time=datetime.now() - timedelta(seconds=61))
@@ -294,7 +295,7 @@ def test_expired_transaction_remove(self):
self.command.run(username='poor_user')
except self.expired_exception:
pass
- count = self.session.query(Transaction).filter_by(id=trans_id).count()
+ count = self.session.query(self.transaction_type).filter_by(id=trans_id).count()
self.assertEqual(count, 0, "Expired transaction not removed")
def test_other_transaction_only(self):
@@ -304,7 +305,7 @@ def test_other_transaction_only(self):
# other transaction record for user 1
self.add_all(
- Transaction(username='poor_user', stock_symbol='ABAB',
+ self.transaction_type(username='poor_user', stock_symbol='ABAB',
operation=other, committed=False, quantity=1,
stock_value=Money(10, 54)),
)
@@ -378,7 +379,6 @@ class TestCOMMIT_SELLCommand(_TransactionCommandTest, DatabaseTest):
def setUp(self):
DatabaseTest.setUp(self)
self._user_fixture()
- self.command = commands.COMMIT_SELLCommand()
# Uncommitted transaction record for user 2 ("user1")
self.trans = Transaction(username='rich_user', stock_symbol='ABAB',
@@ -438,7 +438,6 @@ class TestCANCEL_BUYCommand(_TransactionCommandTest, DatabaseTest):
def setUp(self):
DatabaseTest.setUp(self)
self._user_fixture()
- self.command = commands.CANCEL_BUYCommand()
# Uncommitted transaction record for user 2 ("user1")
self.trans = Transaction(username='rich_user', stock_symbol='ABAB',
@@ -465,7 +464,6 @@ class TestCANCEL_SELLCommand(_TransactionCommandTest, DatabaseTest):
def setUp(self):
DatabaseTest.setUp(self)
self._user_fixture()
- self.command = commands.CANCEL_SELLCommand()
# Uncommitted transaction record for user 2 ("user1")
self.trans = Transaction(username='rich_user', stock_symbol='ABAB',
@@ -484,13 +482,6 @@ def test_postcondition_remove(self):
-
-
-
-
-
-
-
class TestSET_BUY_AMOUNTCommand(DatabaseTest):
def setUp(self):
DatabaseTest.setUp(self)
@@ -589,6 +580,102 @@ def test_postcondition_transaction(self):
self.assertNotEqual(set_transaction, None)
+class TestCANCEL_SET_BUYCommand(DatabaseTest):
+ def setUp(self):
+ DatabaseTest.setUp(self)
+ self.command = commands.CANCEL_SET_BUYCommand()
+
+ self._user_fixture()
+
+ # active BUY trigger for rich_user
+ self.trans = SetTransaction(username='rich_user', stock_symbol='ABAB',
+ operation='BUY', active=False)
+
+ self.add_all(self.trans)
+
+ def test_return_value(self):
+ """ Should return "success" """
+ retval = self.command.run(username='rich_user', stock_symbol='ABAB')
+ self.assertIsInstance(retval, xml.ResultResponse)
+ self.assertEqual(retval.message, "trigger cancelled")
+
+ def test_nonexistent_user(self):
+ """ Should return an error message if the user does not exist """
+ self.assertRaises(commands.UserNotFoundError,
+ self.command.run, username='unicorn', stock_symbol='ABAB')
+
+ def test_nonexistent_trigger(self):
+ """ Should return an error message if user has no matching triggers """
+ self.assertRaises(commands.NoTriggerError,
+ self.command.run, username='poor_user', stock_symbol='ABAB')
+
+ def test_sell_trigger_only(self):
+ """ Should return an error message if user has no matching triggers """
+ # active SELL trigger for poor_user
+ self.add_all(SetTransaction(username='poor_user', stock_symbol='ABAB',
+ operation='SELL', active=False))
+
+ self.assertRaises(commands.NoTriggerError,
+ self.command.run, username='poor_user', stock_symbol='ABAB')
+
+ def test_postcondition_cancelled(self):
+ """ The BUY SetTransaction should be marked as cancelled """
+ self.command.run(username='rich_user', stock_symbol='ABAB')
+
+ transaction = self.session.query(SetTransaction).filter_by(
+ cancelled=False).first()
+ self.assertEqual(transaction, None)
+
+
+class TestCANCEL_SET_SELLCommand(DatabaseTest):
+ def setUp(self):
+ DatabaseTest.setUp(self)
+ self.command = commands.CANCEL_SET_SELLCommand()
+
+ self._user_fixture()
+
+ # active transaction record for rich_user
+ self.trans = SetTransaction(username='rich_user', stock_symbol='ABAB',
+ operation='SELL', active=False, amount=Money(0, 0))
+
+ self.add_all(self.trans)
+
+ def test_return_value(self):
+ """ Should return "success" """
+ retval = self.command.run(username='rich_user', stock_symbol='ABAB')
+ self.assertIsInstance(retval, xml.ResultResponse)
+ self.assertEqual(retval.message, "trigger cancelled")
+
+ def test_nonexistent_user(self):
+ """ Should return an error message if the user does not exist """
+ self.assertRaises(commands.UserNotFoundError,
+ self.command.run, username='unicorn', stock_symbol='ABAB')
+
+ def test_nonexistent_trigger(self):
+ """ Should return an error message if user has no matching triggers """
+ self.assertRaises(commands.NoTriggerError,
+ self.command.run, username='poor_user', stock_symbol='ABAB')
+
+ def test_buy_trigger_only(self):
+ """ Should return an error message if user has no matching triggers """
+ # active BUY trigger for poor_user
+ self.add_all(SetTransaction(username='poor_user', stock_symbol='ABAB',
+ operation='BUY', active=False))
+
+ self.assertRaises(commands.NoTriggerError,
+ self.command.run, username='poor_user', stock_symbol='ABAB')
+
+ def test_postcondition_remove(self):
+ """ The SELL SetTransaction should be removed from the database """
+ self.command.run(username='rich_user', stock_symbol='ABAB')
+
+ # Assume there's no committed / expired transactions
+ transaction = self.session.query(SetTransaction).first()
+ self.assertEqual(transaction, None)
+
+
+
+
class TestDISPLAY_SUMMARY(DatabaseTest):
def setUp(self):
# set up the database as inherited from DatabaseTest
@@ -623,8 +710,10 @@ def test_successful_return_value(self):
res = self.command.run(username='a')
self.assertIsInstance(res, xml.SummaryResponse)
- self.assertEqual(len(res.transactions), 1)
- self.assertEqual(len(res.triggers), 1)
+ self.assertEqual(len(res.transactions), 2)
+ self.assertEqual(len(res.triggers), 2)
self.assertEqual(res.account_balance, self.user.account_balance)
self.assertEqual(res.reserve_balance, self.user.reserve_balance)
+
+
Please sign in to comment.
Something went wrong with that request. Please try again.