root/trac/hacks/marketplugin/0.9/tracmarket/order.py

Revision 118 (checked in by stevegt, 6 years ago)

fix publish button

Line 
1 import time
2
3 from trac.core import implements
4 from trac.db import Table, Column, Index
5 from trac.env import IEnvironmentSetupParticipant
6
7 from tracmarket.api import *
8 from tracmarket.db import *
9 from tracmarket.ledger import ILedger
10 from tracmarket.util import *
11
12 class Order(DbObject):
13     """Generic order header, for double-auction market, limit, stop, or
14     stop limit orders.  Some specialists might need to use a custom
15     order table instead."""
16    
17     # status constants
18     OPEN = 0
19     FILL = 2
20     FILLED = 2
21     CANCEL = 4
22     CANCELLED = 4
23    
24     VERSION = 1
25     TABLE = Table('orders', key=('id'))[
26                 Column('id', auto_increment=True),
27                 # id of last record for this order
28                 Column('tail_id', type='integer'),
29                 Column('symbol'),
30                 Column('currency'),
31                 Column('type'),
32                 # current status -- see constants above
33                 # -- set to sum of order_detail.action
34                 Column('status', type='integer'),
35                 # outstanding unfilled qty
36                 Column('bid_pending', type='numeric'),
37                 Column('ask_pending', type='numeric'),
38                 # total fill qty
39                 Column('total_fill', type='numeric'),
40                 # copy of limit and stop prices
41                 Column('limit_price', type='numeric'),
42                 Column('stop_price', type='numeric'),
43                 # copy of current expiration time
44                 Column('expires', type='numeric'),
45                 # original entity who placed the order
46                 Column('owner'),
47                 # copy of original order entry time
48                 Column('ctime', type='numeric'),
49                 Index(['symbol']),
50                 Index(['currency']),
51                 Index(['status']),
52                 Index(['type']),
53             ]
54     NAME = TABLE.name
55     COLUMNS = [ c.name for c in TABLE.columns ]
56
57     def __getattr__(self, name):
58         if name in self.COLUMNS:
59             return self.get(name, None)
60         return self.tail.get(name)
61
62     def __getitem__(self, name):
63         '''wrap db values in custom types'''
64         # XXX find out why this isn't being called from open_orders()
65         val = super(Order, self).get(name)
66         if name in ('limit_price', 'stop_price'):
67             val = Price(val)
68             return val
69         if name in ('bid_pending', 'ask_pending', 'total_fill'):
70             val = Size(val)
71             return val
72         return val
73
74     def _set_defaults(self):
75         for var in ('status', 'bid_pending', 'ask_pending', 'total_fill'):
76             self[var] = 0
77         for var in ('tail_id', 'total_fill', 'limit_price', 'stop_price',
78                 'expires'):
79             self[var] = None
80         self.chattr(ctime=time.time())
81         self._tail = None
82
83     def _ck_values(self):
84         self._ignore('id', 'tail_id', 'total_fill', 'limit_price', 'stop_price',
85                 'expires')
86
87     def cancel(self, db, ledger, agent):
88         head = self.update(db, ledger, agent, action=self.CANCEL)
89         return head
90
91     def cancelled(self):
92         return self.status == self.CANCELLED
93     cancelled = property(cancelled)
94        
95     def create(cls, env, db, ledger, owner, **kwargs):
96         # insert the header record
97         cursor = db.cursor()
98         symbol=kwargs.get('symbol')
99         currency=kwargs.get('currency')
100         bid=kwargs.get('bid')
101         ask=kwargs.get('ask')
102         assert bid > 0 or ask > 0
103         type = kwargs.pop('type', None) or (bid and 'bid') or 'ask'
104         assert symbol
105         assert currency
106         assert owner
107         head = cls(env, db, symbol=symbol, currency=currency,
108                 type=type, owner=owner)
109         head.insert(cursor=cursor)
110         id = db.get_last_id(cursor, cls.NAME)
111         head = Order.selectone(env, db, id=id)
112         # insert the detail record (which in turn updates the header)
113         det = OrderDetail(env, db, head_id=id, agent=owner, **kwargs)
114         det.insert(db, ledger, owner)
115         assert det.id
116         return head
117     create = classmethod(create)
118
119     def filled(self):
120         return self.status == self.FILLED
121     filled = property(filled)
122        
123     def head(self):
124         return self
125     head = property(head)
126
127     def last_trade(cls, env, db, symbol, currency):
128         """return last Trade obj for symbol and currency"""
129         s = OrderDetail.select(env, db)
130         # for o in s:
131             # print o
132         # get bid side
133         det = OrderDetail.selectone(env, db,
134             append='fill > 0',
135             symbol=symbol, currency=currency,
136             order='mtime DESC', limit=1)
137         if not det:
138             return None
139         if det.side is 'bid':
140             bid = det
141             ask = bid.counterfill
142         else:
143             ask = det
144             bid = ask.counterfill
145         trade = Trade(symbol=bid.symbol, size=bid.fill,
146                 price=bid.fill_price, currency=bid.currency,
147                 time=bid.mtime, bid=bid, ask=ask)
148         return trade
149     last_trade = classmethod(last_trade)
150
151     # alice buys 50 foo @ .23 TMP from bob
152     #
153     # who           account         symbol  debit   credit 
154     # alice         cash            foo     50             
155     # bob           reserve         foo             50     
156     # bob           cash            TMP     11.50         
157     # alice         reserve         TMP             11.50 
158
159     def match(cls, db, ledger, agent, bid, ask, price, basket):
160         """fill two market or limit orders against each other at price"""
161         buyer = bid.owner
162         seller = ask.owner
163         assert buyer != seller
164         assert bid.status == cls.OPEN
165         assert ask.status == cls.OPEN
166         assert bid.symbol == ask.symbol
167         assert bid.currency == ask.currency
168         symbol = bid.symbol
169         currency = bid.currency
170         size = min(bid.bid_pending, ask.ask_pending)
171         assert size > 0
172         assert price > 0
173         total = size * price
174         legs = []
175         memo = 'match: %s sells %s %.2f %s at %.2f %s' % (
176             seller, buyer, size, symbol, price, currency)
177         # print memo, basket
178         if not basket:
179             legs.append(ledger.mkleg(db, entity=buyer,
180                     account='available', debit=size, symbol=symbol))
181             legs.append(ledger.mkleg(db, entity=seller,
182                     account='reserve', credit=size, symbol=symbol))
183         else:
184             # condense basket into one leg on specialist side
185             # print seller, agent
186             if seller == agent:
187                 # specialist sold a basket
188                 legs.append(ledger.mkleg(db, entity=seller,
189                         account='issued', debit=size, symbol=symbol))
190                 legs.append(ledger.mkleg(db, entity=seller,
191                         account='reserve', credit=size, symbol=symbol))
192                 # trader bought components
193                 for contract in basket:
194                     legs.append(ledger.mkleg(db, entity=buyer,
195                         account='available', debit=size, symbol=contract))
196                     legs.append(ledger.mkleg(db, entity=seller,
197                         account='minted', credit=size, symbol=contract))
198             if buyer == agent:
199                 # specialist bought back a basket
200                 legs.append(ledger.mkleg(db, entity=buyer,
201                         account='minted', debit=size, symbol=symbol))
202                 legs.append(ledger.mkleg(db, entity=buyer,
203                         account='issued', credit=size, symbol=symbol))
204                 # trader sold components
205                 for contract in basket:
206                     legs.append(ledger.mkleg(db, entity=buyer,
207                         account='minted', debit=size, symbol=contract))
208                     legs.append(ledger.mkleg(db, entity=seller,
209                         account='reserve', credit=size, symbol=contract))
210         # currency legs
211         legs.append(ledger.mkleg(db, entity=seller,
212                 account='available', debit=total, symbol=currency))
213         # Catch NSF and convert to cancellation:  We only need to do
214         # this on the bid side, because (as of this writing),
215         # OrderDetail.insert always reserves contracts on the ask
216         # side.  It never reserves currency on the bid side if it's a
217         # market order, so that's where NSFs will usually come from.
218         if bid.limit_price:
219             # for limit orders, first try reserve, then cash
220             accounts = ('reserve', 'available')
221         else:
222             # market orders never made a reservation, so just try cash
223             # (don't take reserve funds away from other limit orders)
224             accounts = ('available')
225         leg = None
226         for account in accounts:
227             try:
228                 leg = ledger.mkleg(db, entity=buyer,
229                         account=account, credit=total, symbol=currency)
230                 break
231             except InsufficientFunds:
232                 continue
233         if not leg:
234             # ...no funds anywhere; cancel the ask order
235             assert not ask.limit_price
236             ask.cancel(db, ledger, agent)
237         else:
238             legs.append(leg)
239             # ok so far; post the ledger transaction
240             # for leg in legs: print leg
241             xid = ledger.post(db, agent, legs, memo=memo)
242             # and update the orders
243             bid = bid.update(db, ledger, agent, xid=xid,
244                     basket=basket, fill=size, fill_price=price)
245             ask = ask.update(db, ledger, agent, xid=xid,
246                     basket=basket, fill=size, fill_price=price)
247             # print bid,ask
248         return (bid, ask)
249     match = classmethod(match)
250
251     def market_orders(cls, env, db, side, symbol, currency):
252         return cls.open_orders(env, db, side,
253                 symbol=symbol, currency=currency, order='ctime',
254                 append="limit_price is NULL AND stop_price is NULL")
255     market_orders = classmethod(market_orders)
256
257     def limit_orders(cls, env, db, side, symbol, currency):
258         assert side in ('bid', 'ask')
259         if side == 'bid':
260             order = 'limit_price DESC, ctime'
261         else:
262             order = 'limit_price, ctime'
263         return cls.open_orders(env, db, side,
264                 symbol=symbol, currency=currency, order=order,
265                 append="limit_price not NULL AND stop_price is NULL")
266     limit_orders = classmethod(limit_orders)
267
268     def stop_orders(cls, env, db, side, symbol, currency):
269         assert side in ('bid', 'ask')
270         if side == 'bid':
271             order = 'stop_price DESC, ctime'
272         else:
273             order = 'stop_price, ctime'
274         return cls.open_orders(env, db, side,
275                 symbol=symbol, currency=currency, order=order,
276                 append="stop_price not NULL")
277     stop_orders = classmethod(stop_orders)
278
279     def open_orders(cls, env, db, side=None, **kwargs):
280         """return a generator of all open orders matching kwargs"""
281         if side:
282             append = kwargs.pop('append', '')
283             if append:
284                 append += " AND "
285             if side == 'bid':
286                 append += 'bid_pending > 0'
287             elif side == 'ask':
288                 append += 'ask_pending > 0'
289             kwargs['append'] = append
290         heads = cls.select(env, db, status=0, **kwargs)
291         for head in heads:
292             # print type(head.limit_price)
293             yield head
294     open_orders = classmethod(open_orders)
295
296     def open_symbols(cls, env, db, prefix, currency):
297         """return a generator of all symbols starting with prefix
298         which have open orders"""
299         append='status=0 AND currency="%s" AND symbol LIKE "%s%%"' \
300                 % (currency, prefix)
301         s = Select(env, db, Order, columns='DISTINCT symbol', append=append)
302         orders = s.objs
303         for order in orders:
304             yield order.symbol
305     open_symbols = classmethod(open_symbols)
306
307     def refresh(self):
308         env = self._env
309         db = self._db
310         # get sums from detail records
311         s = Select(env, db, OrderDetail, columns='sum(fill), sum(action)',
312                 head_id=self.id)
313         row = s.cursor().fetchone()
314         fill = row[0] or 0
315         status = int(row[1] or 0)
316         kw = {}
317         def chattr(**attr): kw.update(attr)
318         chattr(status=status)
319         # get last detail record
320         det = OrderDetail.selectone(env, db,
321                 head_id=self.id, order='mtime DESC', limit=1)
322         if det:
323             chattr(tail_id=det.id)
324             if det.side == 'bid':
325                 chattr(bid_pending = det.bid - fill)
326             else:
327                 chattr(ask_pending = det.ask - fill)
328             chattr(total_fill=fill)
329             # clear triggers so specialist knows to review
330             chattr(limit_price=det.limit_price)
331             chattr(stop_price=det.stop_price)
332             chattr(expires=det.expires)
333             if self.ctime is None:
334                 chattr(ctime=det.mtime)
335         super(Order, self).update(**kw)
336         self._tail = None
337         # print "REFRESH", self
338         return self
339
340     def side(self):
341         return self.type
342     side = property(side)
343
344     def tail(self):
345         if self._tail:
346             return self._tail
347         if not self.tail_id:
348             return None
349         tail = OrderDetail.selectone(self._env, self._db, id=self.tail_id)
350         self._tail = tail
351         return tail
352     tail = property(tail)
353
354     def XXXtrades(self, limit=None):
355         head = self
356         fills = OrderDetail.select(self._env, self._db,
357                 head_id=head.id, append='fill_price>0',
358                 order='mtime DESC', limit=limit)
359         trades = []
360         # get counterparty's order record for each fill
361         for me in fills:
362             you = me.counterfill
363             assert me.fill == you.fill
364             if me.fill > 0:
365                 bid = me
366                 ask = you
367             else:
368                 bid = you
369                 ask = me
370             assert bid.symbol == ask.symbol
371             assert bid.currency == ask.currency
372             assert bid.fill_price == ask.fill_price
373             trade = Trade(symbol=bid.symbol, size=bid.fill,
374                     price=bid.fill_price, currency=bid.currency,
375                     time=bid.mtime, bid=bid, ask=ask)
376             trades.append(trade)
377         trades.sort(by_time)
378         return trades
379     # trades = property(trades)
380
381     def update(self, *args, **kw):
382         new = self.tail.update(*args, **kw)
383         return new
384
385     def XXXupdate(self, db, ledger, agent, **kw):
386         if self.tail:
387             head = self.tail.update(db, ledger, agent, **kw)
388             return head
389         else:
390             self._env.log.warning("Order %d: missing detail record; cancelling"
391                     % self.id)
392             det = OrderDetail(self._env, db, head_id=self.id,
393                     agent=self.owner, symbol=self.symbol,
394                     currency=self.currency, action=self.CANCEL)
395             head = det.insert(db, ledger, self.owner)
396             return head
397
398 class OrderDetail(DbObject):
399     """Generic order detail table, for double-auction market, limit,
400     stop, or stop limit orders.  Some specialists might need to use a
401     custom version of this instead."""
402    
403     # this table is insert only -- should never need to do updates in here
404     VERSION = 1
405     TABLE = Table('order_detail', key=('id'))[
406                 Column('id', auto_increment=True),
407                 # id of header record for this order
408                 Column('head_id', type='integer'),
409                 # xid is transaction id returned from ILedger implementor
410                 Column('xid', type='integer'),
411                 # don't change symbol
412                 Column('symbol'),
413                 # don't change currency
414                 Column('currency'),
415                 # current action -- see status constants in Order
416                 Column('action', type='integer'),
417                 # bid size
418                 # set to sum(fill) on cancel
419                 Column('bid', type='numeric'),
420                 # ask size
421                 # set to sum(fill) on cancel
422                 Column('ask', type='numeric'),
423                 # fill size for this detail record
424                 Column('fill', type='numeric'),
425                 # price where fill took place
426                 Column('fill_price', type='numeric'),
427                 # set limit and stop price to NULL for market orders
428                 Column('limit_price', type='numeric'),
429                 Column('stop_price', type='numeric'),
430                 # expiration time is seconds since epoch, NULL for GTC
431                 Column('expires', type='numeric'),
432                 # entity who caused this change
433                 Column('agent'),
434                 Column('comment'),
435                 Column('mtime', type='numeric'),
436             ]
437     NAME = TABLE.name
438     COLUMNS = [ c.name for c in TABLE.columns ]
439
440     def _set_defaults(self):
441         self._head = None
442         self._counterfill = None
443         self._basket = None
444         for var in ('action', 'bid', 'ask', 'fill'):
445             self[var] = 0
446         for var in ('xid',
447                 'limit_price', 'stop_price', 'fill_price', 'expires',
448                 'comment'):
449             self[var] = None
450         self.chattr(mtime=time.time())
451
452     def _ck_values(self):
453         self._ignore('id', 'xid', 'limit_price', 'stop_price', 'fill_price',
454                 'expires', 'comment')
455         action = self.action
456         d = self
457         assert d.id is None
458         head = d.head
459         tail = head.tail
460         if not tail:
461             class NoTail(object): pass
462             tail = NoTail()
463             tail.bid=0
464             tail.ask=0
465             tail.limit_price=None
466         if head.status != 0:
467             raise ValueError("can't update: order is closed: %s" % head)
468         # never use the same action constant twice
469         assert not action & head.status
470         assert d.bid >= 0
471         assert d.ask >= 0
472         assert d.fill >= 0
473         assert not ((tail.bid + d.bid) and (tail.ask + d.ask))
474         if (d.bid and d.bid != tail.bid) or \
475            (d.ask and d.ask != tail.ask):
476             # we're changing order size
477             if (d.bid or tail.bid) and (d.limit_price or tail.limit_price):
478                 assert d.xid
479             assert d.fill == 0
480         if d.fill:
481             assert d.xid
482             assert d.fill_price > 0
483             assert d.bid == tail.bid and d.ask == tail.ask
484         total_fill = (head.total_fill or 0) + d.fill
485         if (tail.bid and d.bid <= total_fill) or \
486            (tail.ask and d.ask <= total_fill):
487             # we've either reduced size or increased fill -- either
488             # way, this order's done
489             # print "ACTION", tail.bid, d.bid, total_fill, action
490             if not action > 0:
491                 raise AssertionError, \
492                     "action unfilled: \n" + \
493                     "head = %s, \n" % head + \
494                     "total_fill = %f, \n" % total_fill + \
495                     "tail = %s,\n" % tail + \
496                     "new = %s" % d
497         if action == head.CANCEL:
498             assert not d.fill
499        
500     def closed(self):
501         if self.head.status > 0:
502             return True
503         return False
504     closed = property(closed)
505
506     def counterfill(self):
507         """find the counterparty for this detail record"""
508         if self._counterfill:
509             return self._counterfill
510         assert self.xid
511         assert self.fill_price
512         s = OrderDetail.select(self._env, self._db,
513             xid=self.xid,
514             append='id != %d AND fill_price = %f' % (self.id, self.fill_price),
515             order='mtime DESC')
516         try:
517             other = s.next()
518         except StopIteration:
519             assert False, "unable to find counterfill for %d" % self.id
520         try:
521             s.next()
522             assert False, "found multiple counterfill for %d" %  self.id
523         except StopIteration:
524             pass
525         self._counterfill = other
526         return other
527     counterfill = property(counterfill)
528
529     def head(self):
530         """return the header record for this order"""
531         if self._head:
532             return self._head
533         head = Order.selectone(self._env, self._db, id=self.head_id)
534         self._head = head
535         return head
536     head = property(head)
537
538     def insert(self, db, ledger, agent):
539         head = self.head
540         owner = head.owner
541         symbol = head.symbol
542         currency = head.currency
543         cursor = db.cursor()
544
545
546         # XXX just insert the order in the db; check and cancel or
547         # fill it later; need to add an 'entered' status code, and
548         # need to add a priority column so we can automatically insert
549         # basket buys in front of short sells
550         # XXX move below to order processing
551
552         # basket list, if any, gets passed here from
553         # Specialist.order() -- dig it out; if we have it then that
554         # means that we may need to break the reservation up into the
555         # contract components
556         basket = self.pop('basket', None)
557
558         # if head.id == 6 and self.action == head.CANCEL:
559         #    import pdb; pdb.set_trace();
560
561         new = self
562         price_delta = (new.limit_price or 0) - (head.limit_price or 0)
563         # Don't allow price changes; those would e.g. enable someone to
564         # keep an old, deeply priced order laying around, then pounce
565         # on inside market. 
566         if head.limit_price and price_delta:
567             raise ValueError, "can't change price on limit orders"
568         if new.side == 'bid':
569             size_delta = new.bid - (head.bid_pending + (head.total_fill or 0))
570             delta = size_delta * new.limit_price
571             # this is a little screwy -- when a trader changes an order,
572             # we change the margin reserve here; but when two orders
573             # get matched, the reserve transfer is handled by
574             # Order.match() instead, which then calls update()... so we
575             # have to ignore fills here.  XXX clean up this mess
576             if delta and not new.fill:
577                 # change cash reserve -- this only happens on limit
578                 # orders
579                 xid = self._reserve(db, ledger, agent, currency, delta,
580                     memo='%s bid reserve %.2f %s at %.2f: %.2f %s' % (
581                         owner, size_delta, symbol,
582                         new.limit_price, delta, currency))
583                 new.chattr(xid=xid)
584         elif new.side == 'ask':
585             delta = new.ask - (head.ask_pending + (head.total_fill or 0))
586             if delta and not new.fill:
587                 # change contract reserve -- this happens on both
588                 # market and limit orders; any basket breakup is
589                 # handled by _reserve()
590                 xid = self._reserve(db, ledger, agent, symbol, delta,
591                     memo='%s ask reserve %.2f %s' % (
592                         owner, delta, symbol),
593                     basket=basket)
594                 new.chattr(xid=xid)
595         else:
596             raise TracError, "invalid order type: %s" % new.side
597
598         # XXX move above to order processing
599
600         # post the order in the order book
601         super(OrderDetail, self).insert(db, cursor=cursor);
602         id = db.get_last_id(cursor, self.NAME)
603         self.chattr(id=id)
604         # print "INSERT", new
605         # summarize detail records into this order's header record
606         head = head.refresh()
607         self._head = None # force a db lookup
608         # print "INSERT HEAD", head
609         return head
610
611     # alice places bid order for 50 foo @ .23 TMP
612     #
613     # who           account         symbol  debit   credit 
614     # alice         reserve         TMP     11.50         
615     # alice         cash            TMP             11.50 
616
617     # bob places ask order for 50 foo @ .23 TMP
618     #
619     # who           account         symbol  debit   credit
620     # bob           reserve         foo     50             
621     # bob           cash            foo             50     
622
623     def _reserve(self, db, ledger, agent, symbol, amount, memo,
624             basket=None):
625         assert amount
626         head = self.head
627         owner = head.owner
628         assert owner
629
630         # print memo
631
632         # try reserving simple symbol first... (this will work if trader has
633         # enough in account; if so they're probably the specialist)
634         working_basket = [symbol]
635         while True:
636             legs = []
637             try:
638                 for symbol in working_basket:
639                     if amount > 0:
640                         # increase margin reserve
641                         total = amount
642                         legs.append(ledger.mkleg(db, entity=head.owner,
643                             account='reserve', debit=total, symbol=symbol))
644                         legs.append(ledger.mkleg(db, entity=head.owner,
645                             account='available', credit=total, symbol=symbol))
646                     else:
647                         # decrease margin reserve
648                         total = abs(amount)
649                         legs.append(ledger.mkleg(db, entity=head.owner,
650                             account='reserve', credit=total, symbol=symbol))
651                         legs.append(ledger.mkleg(db, entity=head.owner,
652                             account='available', debit=total, symbol=symbol))
653                 break
654             except InsufficientFunds:
655                 if not basket:
656                     raise
657                 if working_basket == basket:
658                     raise
659                 # ...then try reserving basket components
660                 working_basket = basket
661
662         xid = ledger.post(db, agent, legs, memo=memo)
663         return xid
664
665     def side(self):
666         return self.head.side
667     side = property(side)
668
669     def update(self, db, ledger, agent, **kwargs):
670         env = self._env
671         self._head = None # force a db lookup
672         head = self.head 
673         attr = self.copy()
674
675         attr.pop('id')
676         attr.pop('xid')
677         attr.pop('fill')
678         attr.pop('mtime')
679         assert self.head_id == attr['head_id']
680         attr.pop('head_id')
681         attr.update(kwargs)
682         # create new order detail record
683         new = OrderDetail(env, db,
684                 id=None, head_id=head.id,
685                 **attr)
686
687         # if head.id == 4:
688         #     import pdb; pdb.set_trace();
689
690         action = new.action or head.OPEN
691         if new.side == 'bid':
692             if head.total_fill + new.fill >= new.bid:
693                 action |= head.FILLED
694                 new.chattr(action=action)
695         else:
696             if head.total_fill + new.fill >= new.ask:
697                 action |= head.FILLED
698                 new.chattr(action=action)
699
700         if new.action & head.CANCEL:
701             if new.side == 'bid':
702                 new.chattr(bid=head.total_fill)
703             else:
704                 new.chattr(ask=head.total_fill)
705        
706         head = new.insert(db, ledger, agent=agent)
707         self._head = None # force a db lookup
708         return head
709
710 class Book(object):
711     """Generic double-auction quote book of all open limit orders for
712     symbol in currency."""
713
714     def __init__(self, env, db, symbol, currency):
715         self.env = env
716         self.db = db
717         self.symbol=symbol
718         self.currency=currency
719
720     def bids(self):
721         """Return generator for bid limit orders, sorted by descending
722         price and ascending time."""
723         # XXX market orders come out at wrong end of list
724         orders = Order.open_orders(self.env, self.db,
725             side='bid',
726             symbol=self.symbol, currency=self.currency,
727             append='stop_price is NULL AND limit_price not NULL',
728             order='limit_price DESC, ctime')
729         return orders
730     bids = property(bids)
731
732     def asks(self):
733         """Return generator for ask limit orders, sorted by ascending
734         price and ascending time."""
735         # XXX market orders come out at wrong end of list
736         orders = Order.open_orders(self.env, self.db,
737             side='ask',
738             symbol=self.symbol, currency=self.currency,
739             append='stop_price is NULL AND limit_price not NULL',
740             order='limit_price, ctime')
741         return orders
742     asks = property(asks)
743
744 class Quote(object):
745     """Generic quote object.  Some specialists might use a child class
746     of this instead."""
747
748     def __init__(self, env, db, symbol, currency, last_count=1):
749         self.env = env
750         self.db = db
751         self.symbol = symbol
752         self.currency = currency
753         self.last_count = last_count
754         self.book = Book(env, db, symbol, currency)
755
756     def last(self):
757         """Return last trade obj."""
758         return Order.last_trade(self.env, self.db,
759                 symbol=self.symbol, currency=self.currency)
760     last = property(last)
761
762     def bidpop(self):
763         """Pop and return highest bid order or None."""
764         try:
765             return self.book.bids.next()
766         except StopIteration:
767             return None
768
769     def askpop(self):
770         """Pop and return lowest ask order or None."""
771         try:
772             return self.book.asks.next()
773         except StopIteration:
774             return None
775
776 class Trade(dict):
777     """Generic trade object."""
778
779     def __init__(self, **kwargs):
780         self.update(kwargs)
781         assert self.symbol
782         assert self.size
783         assert self.price
784         assert self.currency
785         assert self.time
786         assert self.bid
787         assert self.ask
788
789     def __getattr__(self, name):
790         return self[name]
791
792 class InitOrder(InitDB):
793     implements(IEnvironmentSetupParticipant)
794     dbobjects = (Order, OrderDetail)
795
796
797
798
Note: See TracBrowser for help on using the browser.