# -*- coding: UTF-8 -*-
# models.py
#
# Copyright (C) 2014 HES-SO//HEG Arc
#
# Author(s): Cédric Gaspoz <cedric.gaspoz@he-arc.ch>
#
# This file is part of MarMix.
#
# MarMix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# MarMix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with MarMix. If not, see <http://www.gnu.org/licenses/>.
# Stdlib imports
from django.utils import timezone
# Core Django imports
from django.db import models, transaction
from django.utils.translation import ugettext_lazy as _
# Third-party app imports
from django_extensions.db.models import TimeStampedModel
# MarMix imports
from simulations.models import Simulation, Team
from .tasks import match_orders, check_matching_orders
[docs]class Stock(TimeStampedModel):
"""
Stocks are shares of a company that are automatically generated during the simulation setup.
"""
simulation = models.ForeignKey(Simulation, verbose_name=_("simulation"), related_name="stocks", help_text=_("Related simulation"))
symbol = models.CharField(verbose_name=_("symbol"), max_length=4, help_text=_("Symbol of the stock (4 chars)"))
name = models.CharField(verbose_name=_("name"), max_length=100, help_text=_("Full name of the stock"))
description = models.TextField(verbose_name=_("description"), blank=True, help_text=_("Description of the stock (HTML)"))
quantity = models.IntegerField(verbose_name=_("quantity"), default=1, help_text=_("Total quantity of stocks in circulation"))
class Meta:
verbose_name = _('stock')
verbose_name_plural = _('stocks')
ordering = ['symbol']
def _last_quote(self):
try:
last_quote = self.quotes.all()[0]
except IndexError:
last_quote = None
return last_quote
last_quote = property(_last_quote)
def __str__(self):
return self.symbol
[docs]class Quote(models.Model):
"""
Quotes are the price of a given stock at a certain time.
"""
stock = models.ForeignKey('Stock', verbose_name=_("stock"), related_name="quotes", help_text=_("Related stock"))
price = models.DecimalField(verbose_name=_("stock price"), max_digits=14, decimal_places=4,
default='0.0000', help_text=_("Current stock price"))
timestamp = models.DateTimeField(verbose_name=_("timestamp"), auto_now_add=True, help_text=_("Timestamp of the quote"))
class Meta:
verbose_name = _('quote')
verbose_name_plural = _('quotes')
ordering = ['-timestamp']
def __str__(self):
return "%s - %s (%s)" % (self.stock, self.price, self.timestamp)
[docs]class Order(models.Model):
"""
Orders are made by teams and posted in the order book. They are then processed by the order manager.
BID is the highest price that a buyer is willing to pay for a stock.
ASK is the lowest price that a seller is willing to accept for a stock.
"""
BID = 'BID'
ASK = 'ASK'
SIMULATION_TYPE_CHOICES = (
(BID, _('bid')),
(ASK, _('ask')),
)
stock = models.ForeignKey('Stock', verbose_name=_("stock"), related_name="orders", help_text=_("Related stock"))
team = models.ForeignKey(Team, verbose_name=_("team"), related_name="orders", help_text=_("Team which placed the order"))
order_type = models.CharField(verbose_name=_("type of order"), max_length=5, choices=SIMULATION_TYPE_CHOICES,
default=BID, help_text=_("The type of order (bid/ask)"))
quantity = models.IntegerField(verbose_name=_("quantity ordered"), default=0, help_text=_("Quantity ordered. If the total quantity can not be provided, a new order will be created with the balance"))
price = models.DecimalField(verbose_name=_("price tag"), max_digits=14, decimal_places=4,
blank=True, null=True, help_text=_("Price tag for one stock. If NULL, best available price"))
created_at = models.DateTimeField(verbose_name=_("created"), auto_now_add=True, help_text=_("Creation of the order"))
transaction = models.ForeignKey('Transaction', verbose_name=_("transaction"), related_name="orders", null=True,
blank=True, help_text=_("Related transaction"))
class Meta:
verbose_name = _('order')
verbose_name_plural = _('orders')
ordering = ['price', 'created_at']
def __str__(self):
if self.order_type == self.BID:
quantity = self.quantity
else:
quantity = -1 * self.quantity
return "%s: %s@%s" % (self.stock, quantity, self.price)
[docs] def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if self.transaction is None:
models.Model.save(self, force_insert, force_update, using, update_fields)
check_matching_orders.apply_async([self])
else:
models.Model.save(self, force_insert, force_update, using, update_fields)
[docs]class Transaction(models.Model):
"""
Transactions are the result of order placed that are fulfilled.
"""
ORDER = 'ORDER'
EOR = 'EOR'
INITIAL = 'INITIAL'
EOS = 'EOS'
TRANSACTION_TYPE_CHOICES = (
(ORDER, _('stocks order fulfillment')),
(EOR, _('end of round transactions')),
(INITIAL, _('initial transactions')),
(EOS, _('end of simulation transactions')),
)
simulation = models.ForeignKey(Simulation, verbose_name=_("simulation"), related_name="transactions",
help_text=_("Related simulation"))
fulfilled_at = models.DateTimeField(verbose_name=_("fulfilled"), auto_now_add=True, help_text=_("Fulfillment date"))
transaction_type = models.CharField(verbose_name=_("type of transaction"), max_length=20,
choices=TRANSACTION_TYPE_CHOICES, default=ORDER,
help_text=_("The type of transaction"))
class Meta:
verbose_name = _('transaction')
verbose_name_plural = _('transactions')
ordering = ['-fulfilled_at']
def __str__(self):
return "%s" % self.id
[docs]class TransactionLine(models.Model):
STOCKS = 'STOCKS'
DIVIDENDS = 'DIVIDENDS'
TRANSACTIONS = 'TRANSACTIONS'
INTERESTS = 'INTERESTS'
CASH = 'CASH'
ASSET_TYPE_CHOICES = (
(STOCKS, _('stocks')),
(DIVIDENDS, _('dividends payment')),
(TRANSACTIONS, _('costs of transaction')),
(INTERESTS, _('interests payment')),
(CASH, _('cash deposit')),
)
transaction = models.ForeignKey('Transaction', verbose_name=_("transaction"), related_name="lines",
help_text=_("Related transaction"))
stock = models.ForeignKey('Stock', verbose_name=_("stock"), related_name="transactions", null=True, blank=True,
help_text=_("Related stock"))
team = models.ForeignKey(Team, verbose_name=_("team"), related_name="transactions", help_text=_("Team"))
quantity = models.IntegerField(verbose_name=_("quantity"), default=0, help_text=_("Quantity"))
price = models.DecimalField(verbose_name=_("price tag"), max_digits=14, decimal_places=4,
blank=True, null=True, help_text=_("Price tag for one stock"))
amount = models.DecimalField(verbose_name=_("amount"), max_digits=14, decimal_places=4,
blank=True, null=True, help_text=_("Total amount (signed)"))
asset_type = models.CharField(verbose_name=_("type of asset"), max_length=20, choices=ASSET_TYPE_CHOICES,
default=STOCKS, help_text=_("The type of asset"))
class Meta:
verbose_name = _('transaction line')
verbose_name_plural = _('transaction lines')
ordering = ['-transaction_id']
def __str__(self):
return "%s-%s" % (self.transaction_id, self.id)
[docs]def create_generic_stocks(simulation):
char_shift = 65
stocks_created = 0
for i in range(0, simulation.ticker.nb_companies):
symbol = 2*chr(i+char_shift)
stock = Stock(simulation=simulation, symbol=symbol, name='Company %s' % symbol, quantity=10000)
stock.save()
stocks_created += 1
return stocks_created
[docs]def process_opening_transactions(simulation):
# deposit cash
cash_deposit = Transaction(simulation=simulation, transaction_type=Transaction.INITIAL)
cash_deposit.save()
for team in simulation.teams.all():
if team.team_type == Team.PLAYERS:
amount = simulation.capital
elif team.team_type == Team.LIQUIDITY_MANAGER:
amount = 10000*simulation.capital
else:
amount = 0
deposit = TransactionLine(transaction=cash_deposit, team=team, quantity=1, price=amount,
amount=amount, asset_type=TransactionLine.CASH)
deposit.save()
# deposit stocks
# TODO: Calculate opening price!
for stock in simulation.stocks.all():
stocks_deposit = Transaction(simulation=simulation, transaction_type=Transaction.INITIAL)
stocks_deposit.save()
stock_quantity = stock.quantity
allocation = int(stock_quantity*.1/(simulation.teams.count()-1))
for team in simulation.teams.all():
if team.team_type == Team.PLAYERS:
quantity = allocation
elif team.team_type == Team.LIQUIDITY_MANAGER:
quantity = stock_quantity - ((simulation.teams.count()-1) * allocation)
else:
quantity = 0
deposit = TransactionLine(transaction=stocks_deposit, team=team, quantity=quantity, price=0, stock=stock,
amount=0, asset_type=TransactionLine.STOCKS)
deposit.save()
return True
@transaction.atomic
[docs]def process_order(simulation, sell_order, buy_order, quantity):
"""
Fulfill two matching orders.
.. note:: This is called by :func:`check_matching_orders`
:param simulation: A simulation object.
:param sell_order: An order object (sell).
:param buy_order: An order object (buy).
:param quantity: The quantity to exchange (could be a partial fulfillment).
:return: Nothing.
"""
transaction = Transaction(simulation=simulation, transaction_type=Transaction.ORDER)
transaction.save()
price = 0
if sell_order.price and not buy_order.price:
price = sell_order.price
elif buy_order.price and not sell_order.price:
price = buy_order.price
elif buy_order.price and sell_order.price:
# TODO: How to choose the price?
if buy_order.created_at <= sell_order.created_at:
price = sell_order.price
else:
price = buy_order.price
if price > 0:
sell = TransactionLine(transaction=transaction, stock=sell_order.stock, team=sell_order.team,
quantity=-1*quantity, price=price, amount=-1*quantity*price,
asset_type=TransactionLine.STOCKS)
buy = TransactionLine(transaction=transaction, stock=buy_order.stock, team=buy_order.team,
quantity=quantity, price=price, amount=quantity*price,
asset_type=TransactionLine.STOCKS)
if sell_order.quantity != quantity:
new_sell_order = Order(stock=sell_order.stock, team=sell_order.team, order_type=sell_order.order_type,
quantity=sell_order.quantity-quantity, price=sell_order.price,
created_at=sell_order.created_at)
new_sell_order.save()
sell_order.quantity = quantity
if buy_order.quantity != quantity:
new_buy_order = Order(stock=buy_order.stock, team=buy_order.team, order_type=buy_order.order_type,
quantity=buy_order.quantity-quantity, price=buy_order.price,
created_at=buy_order.created_at)
new_buy_order.save()
buy_order.quantity = quantity
sell.save()
buy.save()
sell_order.transaction = transaction
sell_order.save()
buy_order.transaction = transaction
buy_order.save()