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

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

fix publish button

Line 
1 import os
2 import re
3 import time
4 import types
5 import urllib
6
7 from trac.core import *
8 from trac.env import IEnvironmentSetupParticipant
9 from trac.util import format_datetime
10
11 from tracmarket.command import *
12 from tracmarket.order import *
13
14 SequenceType = (types.ListType, types.TupleType, types.GeneratorType)
15
16 class Specialist(CommandInterpreter):
17
18     def pre_cmd(self, ctx):
19         '''housekeeping to run before every command'''
20         super(Specialist, self).pre_cmd(ctx)
21         db = ctx.db
22         ctx.res.market.content_url = self.content_url(ctx.symbol)
23         ctx.res.market.book_url = self.book_url(ctx, ctx.symbol)
24         ctx.res.market.basket_url = self.basket_url(ctx.symbol)
25         ctx.res.market.basket_symbol = self.basket_symbol(ctx.symbol)
26         ctx.res.market.basket_price = self.basket_price
27
28     def post_cmd(self, ctx):
29         '''housekeeping to run after every command'''
30         db = ctx.db
31         # symbol = Symbol(ctx, ctx.symbol)
32         symbol = ctx.symbol
33         currency = ctx.currency
34         # only run order matching every 60 seconds
35         timer = Timer.control(env=self.env, db=db, name='match_period')
36         match_period = self.config.get('market', 'match_period', 60)
37         if timer.expired(match_period):
38             timer.reset()
39             # run basket marketmaker
40             self.cmd_make(ctx)
41             # run order matching
42             self.cmd_match(ctx)
43
44     def cmd_book(self, ctx):
45         # XXX use res_book()
46         quote = Quote(self.env, ctx.db, ctx.symbol, ctx.currency)
47         last = quote.last
48         book = quote.book
49         bids = OrderQ(self.exchange_authname, 'bid', book.bids)
50         asks = OrderQ(self.exchange_authname, 'ask', book.asks)
51         if last:
52             ctx.res['market.last_trade'] = dict(last)
53             ctx.res['market.last_trade.ftime'] = format_datetime(
54                     last.time)
55         ctx.res.market.book.bids = bids
56         ctx.res.market.book.asks = asks
57
58     def cmd_buy(self, ctx, qty, price=None):
59         authname = ctx.req.authname
60         db = ctx.db
61         price = price and Price(price)
62         order = self.order(ctx, owner=authname, size=Size(qty),
63                 limit_price=price)
64         ctx.res['market.order'] = dict(order)
65         ctx.res.info("Entered order id %d: %s" % (order.id, ctx.cmd))
66         # XXX need to queue orders first, check and process later --
67         # XXX remove next 4 lines after this is done
68         # run basket marketmaker
69         self.cmd_make(ctx)
70         # run order matching
71         self.cmd_match(ctx)
72     cmd_b = cmd_buy
73     cmd_bid = cmd_buy
74
75     def cmd_cancel(self, ctx, id):
76         """Cancel an order.  Returns order object -- check contents
77         for status."""
78         order = Order.selectone(ctx.env, ctx.db, id=id)
79         # XXX auth ck
80         order.cancel(ctx.db, ctx.ledger, ctx.req.authname)
81         ctx.res['market.cancelled_order'] = dict(order)
82         ctx.res.info("Cancelled order id %d" % (order.id))
83
84     def cmd_last(self, ctx):
85         '''return last trade'''
86         db = ctx.db
87         symbol = ctx.symbol
88         currency = ctx.currency
89         trade = self.quote(db, symbol, currency).last
90         ctx.res['market.last_trade'] = dict(trade)
91
92     def cmd_make(self, ctx):
93         """maintain the basket market for this symbol"""
94         symbol = ctx.symbol
95         if self.res_underlying(ctx, symbol, ck_only=True): 
96             self.basket_marketmaker(ctx, symbol)
97
98     def cmd_match(self, ctx):
99         """match all open orders for this specialist"""
100         (fills, cancels, stops) = self.match(ctx)
101         ctx.res['market.match.fills'] = fills
102         ctx.res['market.match.cancels'] = cancels
103         ctx.res['market.match.stops'] = stops
104         ctx.res['market.match.activity'] = fills + cancels + stops
105
106     def cmd_orders(self, ctx):
107         authname = ctx.req.authname
108         orders = Order.open_orders(ctx.env, ctx.db, owner=authname)
109         i = 0
110         for order in orders:
111             ctx.res['market.orders.%d' % order.id] = dict(order)
112             i += 1
113         ctx.res['market.order_count'] = i
114
115     def cmd_estimate(self, ctx):
116         self.res_estimate(ctx)
117
118     def cmd_sell(self, ctx, qty, price=None):
119         authname = ctx.req.authname
120         db = ctx.db
121         price = price and Price(price)
122         order = self.order(ctx, owner=authname, size=(-1 * Size(qty)),
123                 limit_price=price)
124         ctx.res['market.order'] = dict(order)
125         ctx.res.info("Entered order id %d: %s" % (order.id, ctx.cmd))
126         # XXX need to queue orders first, check and process later --
127         # XXX remove next 4 lines after this is done
128         # run basket marketmaker
129         self.cmd_make(ctx)
130         # run order matching
131         self.cmd_match(ctx)
132     cmd_s = cmd_sell
133     cmd_a = cmd_sell
134     cmd_ask = cmd_sell
135
136     def cmd_show(self, ctx):
137         req = ctx.req
138         entity = req.authname
139         component_symbols = self.component_symbols(ctx, ctx.symbol)
140         basket = list(component_symbols)
141         if not basket:
142             # looking for a particular contract; just show the order book
143             return self.cmd_book(ctx)
144         ctx.res['title'] = ctx.symbol + " basket"
145         ctx.res['market.show_basket'] = 1
146         ctx.res['market.basket_sequence'] = basket
147         # add the basket
148         self.res_basket(ctx, ctx.symbol)
149         # add the basket book
150         self.cmd_book(ctx)
151
152         '''
153         # XXX probably want to replace this entire for loop with the
154         # XXX results of estimate() instead
155         for contract in basket:
156             # add the prices to res
157             quote = Quote(self.env, ctx.db, contract, ctx.currency)
158             bids = OrderQ(self.exchange_authname, 'bid', quote.book.bids)
159             asks = OrderQ(self.exchange_authname, 'ask', quote.book.asks)
160             bid = (len(bids) and bids[0]) or None
161             ask = (len(asks) and asks[0]) or None
162             last = quote.last
163             path = 'market.basket.%s' % contract
164             ctx.res[path].bid = bid and dict(bid)
165             ctx.res[path].ask = ask and dict(ask)
166             ctx.res[path].last = last and dict(last)
167
168             # add the book as well
169             # XXX why don't we just call cmd_book(symbol=contract)?
170             m = re.match(".*\.(\w+)$", contract)
171             if not m:
172                 raise RuntimeError, "unknown contract: %s" % contract
173             version = m.group(1);
174             ctx.res[path].bids = bids
175             ctx.res[path].asks = asks
176             # and trader's positions
177             # XXX near dup of report.cmd_positions
178             positions = self.positions(ctx, entity, contract)
179             for acct,symbol,position in positions:
180                 # ordered by entity,account,symbol
181                 a = dict(acct)
182                 a['symbol'] = symbol
183                 a['position'] = position
184                 ctx.res[path].positions[entity][acct.account] = a
185             '''
186
187     def cmd_update(self, ctx, id, size):
188         authname = ctx.req.authname
189         size = Size(size)
190         order = Order.selectone(ctx.env, ctx.db, id=id)
191         if order.side == 'bid':
192             order = order.update(ctx.db, ctx.ledger, authname, bid=size)
193         else:
194             order = order.update(ctx.db, ctx.ledger, authname, ask=size)
195         ctx.res['market.order'] = dict(order)
196         ctx.res.info("Updated order id %d" % (order.id))
197     cmd_up = cmd_update
198
199     def res_basket(self, ctx, basket_symbol):
200         '''merge underlying and quotes'''
201         res = self.res(ctx).basket
202         if res:
203             return res
204         res_underlying = self.res_underlying(ctx, basket_symbol)
205         res_quotes = self.res_quotes(ctx, basket_symbol)
206         component_symbols = self.component_symbols(ctx, basket_symbol)
207         for symbol in component_symbols:
208             d = dict(res_underlying[symbol])
209             d.update(dict(res_quotes[symbol]))
210             res[symbol] = d
211         return res
212
213     def res_book(self, ctx, symbol):
214         res = self.res(ctx).book[symbol]
215         if res:
216             return res
217         quote = Quote(self.env, ctx.db, symbol, ctx.currency)
218         last = quote.last
219         book = quote.book
220         bids = OrderQ(self.exchange_authname, 'bid', book.bids)
221         asks = OrderQ(self.exchange_authname, 'ask', book.asks)
222         if last:
223             res.last_trade = dict(last)
224             res.last_trade.ftime = format_datetime(last.time)
225         res.bids = bids
226         res.asks = asks
227         return res
228
229     def component_symbols(self, ctx, basket_symbol):
230         res_underlying = self.res_underlying(ctx, basket_symbol)
231         if not res_underlying:
232             return ()
233         return res_underlying.symbols
234
235     def res_quotes(self, ctx, basket_symbol):
236         res = self.res(ctx).quotes
237         if res:
238             return res
239         component_symbols = self.component_symbols(ctx, basket_symbol)
240         total = 0
241         # XXX finish this -- see quotes.py
242         # bus = ctx.bus
243         # for symbol in component_symbols:
244         #     id = subscribe('market.value.%s' % symbol)
245         #     bus.send('market.symbol', Msg(symbol))
246         #     msg = bus.recv(id)
247         #     value = msg.data
248         #     res[symbol].value = Price(value)
249         # XXX deprecate this call to values()
250         quotes = []
251         for symbol in component_symbols:
252             quote = self.quote(ctx.db, symbol=symbol, currency=ctx.currency)
253             last = quote.last or Object(price=None)
254             bid = quote.bidpop() or Object(limit_price=None)
255             ask = quote.askpop() or Object(limit_price=None)
256             res[symbol].bid = bid
257             res[symbol].ask = ask
258             res[symbol].last = last
259             quotes.append(quote)
260         values = self.values(quotes)
261         for symbol,value in values.items():
262             total += value
263             res[symbol].value = Price(value)
264         total = 0
265         high = 0
266         high_symbol = None
267         high_version = None
268         # summarize
269         for symbol in component_symbols:
270             total += res[symbol].value
271             if res[symbol].value > high:
272                 high = res[symbol].value
273                 high_symbol = symbol
274                 high_version = self.parse_symbol(symbol)[1]
275         res.total = Price(total)
276         res.high = Price(high)
277         res.high_symbol = high_symbol
278         res.high_version = high_version
279         return res
280
281     def values(self, quotes):
282         total = 0
283         undef_count = 0
284         values = {}
285         for quote in quotes:
286             symbol = quote.symbol
287             last = quote.last or Object(price=None)
288             bid = quote.bidpop() or Object(limit_price=None)
289             ask = quote.askpop() or Object(limit_price=None)
290             value = last.price or bid.limit_price or ask.limit_price
291             if value is None:
292                 undef_count += 1
293             else:
294                 total += value
295             values[symbol] = value
296         # set default values
297         for quote in quotes:
298             symbol = quote.symbol
299             value = values[symbol]
300             if value is None:
301                 value = (self.basket_price - total) / undef_count
302                 if value <= 0:
303                     value = 0
304                 values[symbol] = value
305             assert values[symbol] is not None
306         return values
307
308
309     def res_estimate(self, ctx):
310         '''Provide estimated cost, for this trader, of raising each
311         contract's bid price to highest in basket, while maintaining
312         total basket value ~= basket_price.  (For example, if
313         ctx.symbol == 'wiki.FooPage.3', then this function will
314         estimate the cost of making each FooPage version the default
315         viewable version of the page.)
316         
317         Math derived from basket.js.
318         '''
319         res = self.res(ctx).estimate
320         if res:
321             return res
322         entity = ctx.req.authname
323         res_positions = self.res_positions(ctx, entity)
324         basket_symbol = self.basket_symbol(ctx.symbol)
325         res_quotes = self.res_quotes(ctx, basket_symbol)
326         component_symbols = self.component_symbols(ctx, basket_symbol)
327         res_book = {}
328         for symbol in component_symbols:
329             res_book[symbol] = self.res_book(ctx, symbol)
330
331         def estimate_bid(symbol, size, price, cost, net, commands, new_pos):
332             price = Price(max(min_price, price))
333             reserve = price * size
334             cost += reserve
335             net += self.basket_price * size - reserve
336             commands.append("%s bid %f %f" % (symbol, size, price))
337             return dict(symbol=symbol, size=size, price=price, cost=cost,
338                     net=net, commands=commands, new_pos=new_pos)
339         def estimate_ask(symbol, size, price, cost, net, commands, new_pos):
340             price = Price(min(max_price, price))
341             reserve = 0
342             baskets_needed = size - new_pos[symbol]
343             commands
344             if baskets_needed > 0:
345                 # buy baskets
346                 for s in new_pos:
347                     new_pos[s] += baskets_needed
348                 commands.append("%s bid %f %f" %
349                         (basket_symbol, baskets_needed, self.basket_price))
350                 reserve += self.basket_price * size
351             cost += reserve
352             # post ask for individual contract
353             new_pos[symbol] -= size
354             # allow for profit from selling off contract
355             net += price * size - reserve
356             commands.append("%s ask %f %f" % (symbol, size, price))
357             return dict(symbol=symbol, size=size, price=price, cost=cost,
358                     net=net, commands=commands, new_pos=new_pos)
359
360         min_price = self.basket_price/100
361         max_price = self.basket_price - min_price
362         new_high = res_quotes.high + (min_price * 1.4)
363
364         for high_symbol in component_symbols:
365             new_total = res_quotes.total - \
366                     res_quotes[high_symbol].value + new_high
367             # init dict to hold new estimated positions
368             new_pos = {}
369             for symbol in component_symbols:
370                 new_pos[symbol] = res_positions.available[symbol] or 0
371             # init simulation env
372             sim = dict(symbol=None, size=None, price=None,
373                     cost=0, net=0, commands=[], new_pos=new_pos)
374             ck_total = 0
375             for symbol in component_symbols:
376                 # calculate new contract value
377                 value = res_quotes[symbol].value
378                 if symbol == high_symbol:
379                     # set new high price
380                     value = new_high
381                 else:
382                     # normalize
383                     value = (value * self.basket_price) / new_total
384                 res[high_symbol].value[symbol] = Price(value)
385                 # simulate orders needed to reach new contract value
386                 sim.update(dict(symbol=symbol))
387                 ck_total += value
388                 if value > res_quotes[symbol].value:
389                     # need to move price up; create bid
390                     for o in res_book[symbol].asks:
391                         # fill existing asks
392                         sim['price'] = o.limit_price
393                         sim['size'] = o.ask_pending
394                         if sim['price'] > value:
395                             # passed best price
396                             break
397                         sim = estimate_bid(**sim)
398                     # set the new inside bid
399                     sim['price'] = value
400                     sim['size'] = 1
401                     sim = estimate_bid(**sim)
402                 elif value < res_quotes[symbol].value:
403                     # need to move price down; create ask
404                     for o in res_book[symbol].bids:
405                         # fill existing bids
406                         sim['price'] = o.limit_price
407                         sim['size'] = o.bid_pending
408                         if sim['price'] < value:
409                             # passed best price
410                             break
411                         sim = estimate_ask(**sim)
412                     # set new inside ask
413                     sim['price'] = value
414                     sim['size'] = 1
415                     sim = estimate_ask(**sim)
416             res[high_symbol].ck_total = ck_total
417             res[high_symbol].cost = sim['cost']
418             res[high_symbol].net = sim['net']
419             res[high_symbol].commands = sim['commands']
420             res[high_symbol].commands_quoted = urllib.quote(
421                     '\n'.join(sim['commands']))
422             res[high_symbol].new_positions = sim['new_pos']
423             res[high_symbol].ratio = round(sim['net']/sim['cost'], 2)
424
425     def res_positions(self, ctx, entity):
426         res = self.res(ctx).positions[entity]
427         if res:
428             return res
429         positions = self.positions(ctx, entity)
430         for acct,symbol,balance in positions:
431             # ordered by entity,account,symbol
432             res[acct.account][symbol] = balance
433         return res
434
435     # backend methods (don't touch res below here)
436         
437     def basket_marketmaker(self, ctx, symbol):
438         env = self.env
439         db = ctx.db
440         currency = ctx.currency
441         ledger = ctx.ledger
442         agent = self.exchange_authname
443         target = self.basket_marketmaker_size
444         price = self.basket_price
445
446         # find out what we have on order right now
447         bids = Order.open_orders(self.env, db, limit_price=price,
448             owner=agent, side='bid', symbol=symbol, currency=currency)
449         asks = Order.open_orders(self.env, db, limit_price=price,
450             owner=agent, side='ask', symbol=symbol, currency=currency)
451         bid = None
452         ask = None
453         bid_pending = 0
454         ask_pending = 0
455         for o in bids:
456             # if o.limit_price != price:
457             #     # cancel mispriced order
458             #     o.cancel(db, ledger, agent)
459             #     continue
460             bid = o
461             bid_pending += bid.bid_pending
462         for o in asks:
463             # if o.limit_price != price:
464             #     # cancel mispriced order
465             #     o.cancel(db, ledger, agent)
466             #     continue
467             ask = o
468             ask_pending += ask.ask_pending
469
470         # handle the bid side
471         # only place enough bids to buy back what we've sold
472         issued = ledger.balance(db, agent, 'issued', symbol)
473         needed = issued - bid_pending
474         size = min(target, needed)
475         # don't exceed available funds
476         available = ledger.balance(db, agent, 'available', currency)
477         size = min(size, available/price)
478         if size > 0:
479             if bid:
480                 # update an existing order (we don't care which one)
481                 size += bid.bid_pending
482                 bid = bid.update(db, ledger, agent,
483                         bid=size)
484                 assert bid
485             else:
486                 # place a new order
487                 self.order(ctx, owner=agent, size=size,
488                         limit_price=Price(price), symbol=symbol)
489
490         # handle the ask side
491         size = target - ask_pending
492         if size > 0:
493             # create baskets
494             a = ledger.mkleg(db, entity=agent, account='available',
495                     debit=size, symbol=symbol)
496             b = ledger.mkleg(db, entity=agent, account='minted',
497                     credit=size, symbol=symbol)
498             ledger.post(db,agent,(a,b), 'mint %s baskets' % symbol)
499             if ask:
500                 # update an existing order (we don't care which one)
501                 size += ask.ask_pending
502                 # print "updating", size
503                 ask = ask.update(db, ledger, agent,
504                         ask=size)
505                 assert ask
506             else:
507                 # create new
508                 size *= -1
509                 self.order(ctx, owner=agent, size=size,
510                         limit_price=Price(price), symbol=symbol)
511
512     def basket_url(self, symbol):
513         return None
514
515     def expire(self, db, ledger, agent, symbol, currency,
516             **kwargs):
517         # cancel expired orders
518         for side in ('bid', 'ask'):
519             expired = Order.open_orders(self.env, db,
520                 side=side,
521                 symbol=symbol, currency=currency,
522                 append='expires < %f' % time.time())
523             for o in expired:
524                 o.cancel(db, ledger, agent)
525
526     def match(self, ctx):
527         env = self.env
528         db = ctx.db
529         ledger = ctx.ledger
530         # these are set by child class
531         currency = self.currency
532         prefix = self.namespace + '.'
533         agent = self.exchange_authname
534         total_fills = 0
535         total_cancels = 0
536         total_stops = 0
537         symbols = Order.open_symbols(env, db, prefix=prefix, currency=currency)
538         for symbol in symbols:
539             # if symbol == 'wiki.FooPage.3':
540             #     import pdb; pdb.set_trace()
541             basket = self.component_symbols(ctx, symbol)
542             kw = dict(db=db, ledger=ledger, agent=agent, basket=basket,
543                     symbol=symbol, currency=currency)
544             redo = False
545             self.expire(**kw)
546             while True:
547                 (market_fills, market_cancels) = self.match_market(**kw)
548                 stops = self.trigger_stops(**kw)
549                 (limit_fills, limit_cancels) = self.match_limit(**kw)
550                 # print "MATCHED LIMIT"
551                 total_fills += market_fills + limit_fills
552                 total_cancels += market_cancels + limit_cancels
553                 total_stops += stops
554                 if market_fills or limit_fills or market_cancels or \
555                         limit_cancels or stops:
556                     continue
557                 break
558         return (total_fills, total_cancels, total_stops)
559    
560     def match_limit(self, db, ledger, agent, symbol, currency, basket):
561         '''fill limit orders'''
562         fills = 0
563         cancels = 0
564         bids = Order.limit_orders(self.env, db,
565             side='bid', symbol=symbol, currency=currency)
566         asks = Order.limit_orders(self.env, db,
567             side='ask', symbol=symbol, currency=currency)
568         # XXX possible memory hog
569         bids = OrderQ(self.exchange_authname, 'bid', bids)
570         asks = OrderQ(self.exchange_authname, 'ask', asks)
571         while True:
572             fill = self.find_fill(db, bids, asks)
573             if not (fill):
574                 break
575             assert fill.bid.limit_price >= fill.price >= fill.ask.limit_price
576             (bid, ask) = Order.match(db, ledger, agent, fill.bid, fill.ask,
577                     price=fill.price, basket=basket)
578             # dequeue if completely filled
579             if not bid.bid_pending:
580                 bids.dq(bid)
581             if not ask.ask_pending:
582                 asks.dq(ask)
583             fills += bid.filled and 1
584             cancels += bid.cancelled and 1
585             fills += ask.filled and 1
586             cancels += ask.cancelled and 1
587         return (fills, cancels)
588
589     def find_fill(self, db, bids, asks):
590         '''returns a Fill object, or None'''
591         # import pdb; pdb.set_trace()
592         if not (len(bids) and len(asks)):
593             return None
594         bid = bids[0]
595         ask = asks[0]
596
597         if bid.limit_price < ask.limit_price:
598             # spread exists
599             return None
600
601         if bid.owner == ask.owner:
602             return self.handle_crossed_orders(db, bids, asks)
603
604         fill = Fill(bid, ask)
605
606         return fill
607
608     def handle_crossed_orders(self, db, bids, asks):
609         '''Called if trader tried to cross their own order -- we can't
610         allow a trade to take place.  We leave both orders on the
611         books, but we ignore one of the two orders by popping it off
612         the queue, and recurse, searching for fills.  If we
613         finally find a good fill, we return a Fill object, just like
614         find_fill().
615         '''
616         # XXX rewrite this whole damn thing -- probably by moving most
617         # XXX of the functionality into OrderQ and Fill
618         ledger = self.ledger
619         agent = self.exchange_authname
620         def pop_discount(bids, asks):
621             def discount(orders):
622                 '''how much price improvement we would lose if
623                 we matched against next order instead of
624                 current'''
625                 if len(orders) < 2:
626                     return Price("infinity")
627                 current = orders[0]
628                 next = orders[1]
629                 price = current.limit_price
630                 next_price = next.limit_price
631                 discount = abs(next_price - price)
632                 return discount
633             bid_discount = discount(bids)
634             ask_discount = discount(asks)
635             if bid_discount <= ask_discount:
636                 bids.pop(0)
637             else:
638                 asks.pop(0)
639         def try_fill(b, a):
640             fill = self.find_fill(db, b, a)
641             if fill:
642                 bids[:] = OrderQ(self.exchange_authname, 'bid', b)
643                 asks[:] = OrderQ(self.exchange_authname, 'ask', a)
644                 return fill
645             return None
646         # make a copy of bids and asks 'cause we're going to modify,
647         # recurse, and retry, sorta like an NFA
648         b = OrderQ(self.exchange_authname, 'bid', bids)
649         a = OrderQ(self.exchange_authname, 'ask', asks)
650         b.reload()
651         a.reload()
652         if not (len(b) and len(a)):
653             bids[:] = b
654             asks[:] = a
655             return None
656         # if bid price is higher than ask...
657         if b[0].limit_price > a[0].limit_price:
658             # we assume trader wants the newer order to prevail --
659             # cancel the older one
660             fill = Fill(b[0], a[0])
661             self.env.log.debug('cancel crossed order: ' +
662                     str(fill.oldest))
663             ledger.post(db,agent,(), 'cancel crossed order %s' % fill.oldest)
664             fill.oldest.cancel(db, ledger, agent)
665             if fill.oldest is b[0]:
666                 b.pop(0)
667             elif fill.oldest is a[0]:
668                 a.pop(0)
669             else:
670                 assert False
671             return try_fill(b, a)
672         # this pops either b or a, not both
673         pop_discount(b, a)
674         # try to fill -- this updates the contents of bids and asks
675         # queues (from b and a) if we find a good fill
676         fill = try_fill(b, a)
677         if fill:
678             return fill
679         # resulted in no match; try popping the newest
680         b = OrderQ(self.exchange_authname, 'bid', bids)
681         a = OrderQ(self.exchange_authname, 'ask', asks)
682         if b[0].ctime > a[0].ctime:
683             b.pop(0)
684         else:
685             a.pop(0)
686         fill = try_fill(b, a)
687         if fill:
688             return fill
689         # ok, try popping the oldest
690         b = OrderQ(self.exchange_authname, 'bid', bids)
691         a = OrderQ(self.exchange_authname, 'ask', asks)
692         if b[0].ctime < a[0].ctime:
693             b.pop(0)
694         else:
695             a.pop(0)
696         fill = try_fill(b, a)
697         if fill:
698             return fill
699         # no joy; give up
700         return None
701
702            
703     def match_market(self, db, ledger, agent, symbol, currency, basket):
704         '''fill market orders'''
705         fills = 0
706         cancels = 0
707         market_bids = Order.market_orders(self.env, db,
708             side='bid', symbol=symbol, currency=currency)
709         market_asks = Order.market_orders(self.env, db,
710             side='ask', symbol=symbol, currency=currency)
711         # XXX possible memory hog
712         market_bids = list(market_bids)
713         market_asks = list(market_asks)
714         if market_bids:
715             asks = Order.limit_orders(self.env, db,
716                 side='ask', symbol=symbol, currency=currency)
717             for ask in asks:
718                 if not market_bids:
719                     break
720                 fill_price = ask.limit_price
721                 assert fill_price
722                 while True:
723                     bid = market_bids.pop(0)
724                     if bid.owner == ask.owner:
725                         # trader tried to hit their own limit order --
726                         # skip their market order
727                         continue
728                 (bid, ask) = Order.match(db, ledger, agent, bid, ask,
729                         price=fill_price, basket=basket)
730                 fills += bid.filled and 1
731                 cancels += bid.cancelled and 1
732                 fills += ask.filled and 1
733                 cancels += ask.cancelled and 1
734         if market_asks:
735             bids = Order.limit_orders(self.env, db,
736                 side='bid', symbol=symbol, currency=currency)
737             for bid in bids:
738                 if not market_asks:
739                     break
740                 fill_price = bid.limit_price
741                 assert fill_price
742                 while True:
743                     ask = market_asks.pop(0)
744                     if bid.owner == ask.owner:
745                         # trader tried to hit their own limit order --
746                         # skip their market order
747                         continue
748                 (bid, ask) = Order.match(db, ledger, agent, bid, ask,
749                         price=fill_price, basket=basket)
750                 fills += bid.filled and 1
751                 cancels += bid.cancelled and 1
752                 fills += ask.filled and 1
753                 cancels += ask.cancelled and 1
754         return (fills, cancels)
755
756     def order(self, ctx, owner, size, **kwargs):
757         """Create an order to buy or sell 'size' items.  Orders with
758         different denominations go into different books; we don't
759         perform automatic currency conversion.  Negative size means
760         sell, positive size means buy.  Kwargs are specific to the
761         instrument being traded.  Returns order object."""
762         env = ctx.env
763         db = ctx.db
764         ledger = ctx.ledger
765         # allow callers to override ctx.symbol by using kwargs
766         kwsymbol = kwargs.pop('symbol',None)
767         if kwsymbol:
768             symbol = kwsymbol
769         else:
770             symbol = ctx.symbol
771         currency = ctx.currency
772         basket = self.component_symbols(ctx, symbol)
773         if not size:
774             raise ValueError("order size can't be zero")
775         if size > 0:
776             order = Order.create(env, db, ledger, owner=owner,
777                     symbol=symbol, currency=currency,
778                     bid = size, basket=basket,
779                     **kwargs)
780         else:
781             order = Order.create(env, db, ledger, owner=owner,
782                     symbol=symbol, currency=currency,
783                     ask = abs(size), basket=basket,
784                     **kwargs)
785         return order
786
787     def parse_symbol(self, symbol):
788         return None, None
789
790     def positions(self, ctx, entity=None, symbol=None):
791         # XXX replace all this with a single sql query
792         ledger = self.ledger
793         db = ctx.db
794         if symbol:
795             symbols = (symbol,)
796         else:
797             symbols = list(ledger.symbols(db))
798             symbols.sort(self.by_symbol)
799         for acct in ledger.accounts(db, entity=entity):
800             for symbol in symbols:
801                 if symbol == ctx.currency:
802                     continue
803                 balance = acct.balance(symbol)
804                 if not balance:
805                     continue
806                 yield acct, symbol, balance
807
808     def quote(self, db, symbol, currency, last_count=1):
809         """Return a consolidated quote containing order book and
810         'last_count' number of trades."""
811         self.env.log.debug("quote() called for " + symbol)
812         quote = Quote(self.env, db, symbol=symbol, currency=currency,
813                 last_count=last_count)
814         return quote
815
816     def trigger_stops(self, db, ledger, agent, symbol, currency, **kwargs):
817         '''convert any triggered stop orders to limit orders'''
818         stops = 0
819         last = None
820         quote = self.quote(db, symbol=symbol, currency=currency)
821         if quote.last:
822             last = quote.last.price
823         if not last:
824             return False
825         bids = Order.stop_orders(self.env, db,
826             side='bid', symbol=symbol, currency=currency)
827         asks = Order.stop_orders(self.env, db,
828             side='ask', symbol=symbol, currency=currency)
829         for bid in bids:
830             if last > bid.stop_price:
831                 # stop has been triggered; remove stop
832                 bid.update(db, ledger, agent,
833                         stop_price=None)
834                 stops += 1
835         for ask in asks:
836             if last < ask.stop_price:
837                 # stop has been triggered; remove stop
838                 ask.update(db, ledger, agent,
839                         stop_price=None)
840                 stops += 1
841         return stops
842
843 class Fill(object):
844     def __init__(self, bid, ask):
845         self.bid = bid
846         self.ask = ask
847
848     def oldest(self):
849         orders = [self.bid, self.ask]
850         def by_ctime(a,b): return cmp(a.ctime, b.ctime)
851         orders.sort(by_ctime)
852         return orders[0]
853     oldest = property(oldest)
854
855     def price(self):
856         # use the oldest limit price; newer orders will be improved
857         return self.oldest.limit_price
858     price = property(price)
859
860 class OrderQ(list):
861     # XXX this should be expanded; move a lot of the matching into here
862
863     def __init__(self, exchange_authname, side, data):
864         assert side in ('bid', 'ask')
865         self.exchange_authname = exchange_authname
866         self.side = side
867         self[:] = list(data)
868         self.refresh()
869    
870     def refresh(self):
871         # remove canceled orders
872         for o in self:
873             if o.status > 0:
874                 self.remove(o)
875         # re-sort
876         self.sort(self.by_priority)
877
878     def reload(self):
879         # reload all orders from db
880         # XXX this should really redo the original SQL query, maybe
881         # after we move this to order.py
882         for o in self:
883             self.remove(o)
884             self.append(o.reload())
885         self.refresh()
886    
887     def dq(self,order):
888         for o in self:
889             if o.id == order.id:
890                 self.remove(o)
891                 break
892
893     def by_priority(self, x, y):
894         # sort by price
895         if x.limit_price != y.limit_price:
896             if self.side == 'bid':
897                 return cmp(y.limit_price, x.limit_price)
898             if self.side == 'ask':
899                 return cmp(x.limit_price, y.limit_price)
900         # put customer orders first
901         if x.owner != y.owner:
902             if x.owner == self.exchange_authname:
903                 return 1
904             if y.owner == self.exchange_authname:
905                 return -1
906         # give up and sort by time
907         return cmp(x.ctime, y.ctime)
908
909 class Object(dict):
910
911     def __init__(self, **kwargs):
912         self.update(kwargs)
913
914     def __getattr__(self, name):
915         # XXX won't pickle without this -- why?
916         if name.startswith('__'):
917             super(Object, self).__getattr__(name)
918         return self[name]
919
920
921
Note: See TracBrowser for help on using the browser.