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

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

checkpoint after mss tutorial, before moving order validity checks until after order entry into db

Line 
1 import math
2 import os
3 import sys
4 import time
5 import types
6 import urllib
7
8 from trac.core import *
9 from trac.db import Table, Column, Index
10 from trac.env import IEnvironmentSetupParticipant
11
12 from tracmarket.api import *
13 from tracmarket.db import *
14 from tracmarket.ledger import ILedger, MissingAccount
15 from tracmarket.mq import *
16 from tracmarket.util import *
17 from tracmarket.response import *
18
19 SequenceType = (types.ListType, types.TupleType, types.GeneratorType)
20
21 class CommandInterpreter(Component):
22     implements(IMessageBusSubscriber)
23
24     default_verb = 'show'
25
26     ledgers = ExtensionPoint(ILedger)
27     interpreters = ExtensionPoint(ICommandInterpreter)
28
29     namespace = None
30
31     def __init__(self):
32         # XXX deprecate
33         self.ledger = iter(self.ledgers).next()
34         self.exchange_authname = self.config.get(
35             'market', 'exchange_authname', 'tracmarket')
36         self.currency = self.config.get('market', 'currency', 'TMP')
37         self.basket_marketmaker_size = Size(self.config.get(
38             'market', 'basket_marketmaker_size', 500))
39         self.basket_price = Price(self.config.get(
40             'market', 'basket_price', 100))
41         agent = self.exchange_authname
42         db = self.env.get_db_cnx()
43         # these exit silently if the accounts already exist in the db
44         self.ledger.mkaccount(db, entity=agent,
45                 account='minted', type='liability',
46                 description='baskets, contracts, and currency created by %s'
47                     % agent)
48         self.ledger.mkaccount(db, entity=agent,
49                 account='issued', type='asset',
50                 description='issued baskets (components held by traders)')
51         self.ledger.mkaccount(db, entity=agent,
52                 account='available', type='asset',
53                 description='available assets')
54         self.ledger.mkaccount(db, entity=agent,
55                 account='reserve', type='asset',
56                 description='reserved assets (order margin etc.)')
57         # XXX do we need this, or does db commit on cnx close?
58         db.commit()
59
60     # IMessageBusSubscriber methods
61
62     def solicit(self, bus):
63         # XXX deprecate in favor of ctl._route_cmd
64         # bus.subscribe('market.cmd', reader=self._run_cmd)
65         gre = re.compile('^market\.cmd\.%s\.*' % self.namespace)
66         bus.subscribe(gre, reader=self._run_cmd)
67
68     def _run_cmd(self, bus, group, msg):
69         # receive a command, parse and execute it; responses go to bus
70         ctx = bus.ctx
71         cmd = msg.data
72         cmx = self.parse_cmd(ctx, cmd)
73         if cmx:
74             cmx()
75
76     # ICommandInterpreter methods
77
78     def parse_cmd(self, ctx, cmd):
79         """Returns a Context object that, when called, will execute
80         cmd.  Returns None if this specialist can't handle this
81         command; if throw=True, then throws an exception explaining
82         why.  Calling the Context object returns a clearsilver
83         template name and optional mime-type (default text/html).
84         Optional context (such as currency) can be requested in req.
85         Any session variables must be carried in req.  Authenticated
86         user, if needed by command, must be available as req.authname.
87         Results are returned as a Response object, suitable for stuffing
88         into hdf if the *_ui module chooses to."""
89         cmx = Cmd(ctx=ctx, cmd=cmd, interpreter=self)
90         return cmx
91
92     # XXX deprecate
93     def res(self, ctx):
94         # return ctx.res.market[self.namespace]
95         return ctx.res.market
96
97     # backend methods (don't touch res below here)
98
99     def by_symbol(self, a, b):
100         if type(a) is dict:
101             a = a['symbol']
102             b = b['symbol']
103         aparts = a.split('.')
104         bparts = b.split('.')
105         for i in range(len(aparts)):
106             apart = aparts[i]
107             if i > len(bparts) - 1:
108                 return 1
109             bpart = bparts[i]
110             try:
111                 apart = float(apart)
112             except:
113                 pass
114             try:
115                 bpart = float(bpart)
116             except:
117                 pass
118             c = cmp(apart, bpart)
119             if c:
120                 return c
121         return cmp(a,b)
122
123     def ck_accounts(self, ctx):
124         # XXX use perms -- for now go ahead and let anonymous play
125         # if authname == 'anonymous':
126         #     return
127         env = self.env
128         db = ctx.db
129         authname = ctx.req.authname
130         ledger = self.ledger
131         currency = self.currency
132         agent = self.exchange_authname
133         # trader account setup
134         try:
135             ledger.normal(db, entity=authname, account='available')
136             ledger.normal(db, entity=authname, account='reserve')
137             ledger.normal(db, entity=authname, account='granted')
138         except MissingAccount:
139             ledger.mkaccount(db, entity=authname,
140                     account='available', type='asset',
141                     description='available assets')
142             ledger.mkaccount(db, entity=authname,
143                     account='reserve', type='asset',
144                     description='reserved assets (order margin etc.)')
145             # currency granted to this trader as seed funding
146             ledger.mkaccount(db, entity=authname,
147                     account='granted', type='liability',
148                     description='grants to %s from %s' % (authname,agent))
149             self.grant(ctx, initial=True)
150
151     def content_url(self):
152         return None
153
154     def basket_symbol(self):
155         return None
156
157     def grant(self, ctx, initial=False):
158         '''(de)allocate seed funds such that each trader has enough to
159         trade one of each existing basket'''
160         env = self.env
161         db = ctx.db
162         authname = ctx.req.authname
163         ledger = self.ledger
164         currency = self.currency
165         agent = self.exchange_authname
166         if authname == agent:
167             # don't grant points to exchange agent
168             return
169         basket_count = 0
170         for interpreter in self.interpreters:
171             if hasattr(interpreter, 'basket_count'):
172                 basket_count += interpreter.basket_count()
173         granted = ledger.balance(db, authname, 'granted', currency)
174         grant = (basket_count * self.basket_price) - granted
175         # provide ability to hardcode minimum available balance
176         # (e.g. training mode)
177         minbal = Price(self.config.get( 'market', 'grant_minimum_balance', '0'))
178         available = ledger.balance(db, authname, 'available', currency)
179         if minbal > 0:
180             mingrant = minbal - available
181         else:
182             mingrant = Price("infinity") * -1
183         grant = max(grant, mingrant)
184         if grant == 0:
185             return
186         reserve = ledger.balance(db, authname, 'reserve', currency)
187         if grant > 0:
188             # increase grant (slowly; discourage manipulation of page count)
189             # don't grant more than 10% of the total page count
190             page_factor = float(
191                     self.config.get('market', 'grant_page_factor', .1))
192             maxgrant = self.basket_price * basket_count * page_factor
193             # don't grant more than 100% of trader's reserve funds
194             reserve_factor = float(
195                     self.config.get('market', 'grant_reserve_factor', 1))
196             maxgrant = min(maxgrant, reserve * reserve_factor)
197             # don't grant newbies more than enough to trade 5 baskets
198             initial_limit = float(
199                     self.config.get('market', 'grant_initial_limit', 5))
200             if initial:
201                 maxgrant = self.basket_price * initial_limit
202             grant = int(min(grant, maxgrant))
203             if initial:
204                 # provide ability to hardcode initial grant
205                 grant = float(
206                     self.config.get('market', 'grant_initial', grant))
207             grant = max(grant, mingrant)
208             if not grant:
209                 return
210             a = ledger.mkleg(db, entity=authname,
211                     account='available', debit=grant, symbol=currency)
212             b = ledger.mkleg(db, entity=authname,
213                     account='granted', credit=grant, symbol=currency)
214             ledger.post(db, agent, (a, b),
215                     '%s grants %f %s seed funding to %s' %
216                     (agent, grant, currency, authname))
217         else:
218             # decrease grant (rapidly; recover quickly from manipulation)
219             no_decrease = int(
220                     self.config.get('market', 'grant_no_decrease', 1))
221             if no_decrease:
222                 return
223             grant = abs(grant)
224             a = ledger.mkleg(db, entity=authname,
225                     account='available', credit=grant, symbol=currency)
226             b = ledger.mkleg(db, entity=authname,
227                     account='granted', debit=grant, symbol=currency)
228             try:
229                 ledger.post(db, agent, (a, b),
230                     '%s reduces %s seed funding liability ' +
231                     '(%d fewer baskets on site)' % (agent, authname, grant))
232             except InsufficientFunds:
233                 # either the trader's out of available funds, or we
234                 # tried to decrease below total granted
235                 pass
236
237     def query_timer(self, ctx, start, end):
238         elapsed = end - start
239         if elapsed > .1:
240             self.env.log.info(
241                 "cmd took %f seconds: %s" % (elapsed, ctx.cmd))
242
243     def pre_cmd(self, ctx):
244         '''housekeeping to run before every command'''
245         db = ctx.db
246         # only run granting once every 24 hours for each trader
247         timer_name = "grant_period_" + ctx.req.authname
248         timer = Timer.control(env=self.env, db=db, name=timer_name)
249         grant_period = self.config.get('market', 'grant_period', 24 * 3600)
250         if timer.expired(grant_period):
251             # granting runs for this trader only
252             self.grant(ctx)
253             timer.reset()
254         # show balance *after* granting
255         ctx.res.market.current_balance = ctx.ledger.balance(
256                 db, ctx.req.authname, 'available', ctx.currency)
257         ctx.res.market.currency = ctx.currency
258
259     def cmd_ping(self, ctx):
260         ctx.res.market.ping = 'ping response ' + str(time.time())
261
262 # XXX deprecate in favor of e.g. traders.py technique
263 class Cmd(dict):
264     '''A command object.
265
266     This parser implements an "object [verb [args]]" grammar.  An
267     object's behavior in the absence of verb or args is
268     class-dependent. 
269     
270     Example commands:
271
272     show FooPage order book:
273     
274         wiki.FooPage
275     
276     buy 50 FooPage version 3 @ .23 TMP:
277
278         wiki.FooPage.3 b 50 .23 TMP
279
280     cancel order 1234:
281
282         order.1234 cancel
283
284     show portfolio:
285
286         portfolio
287     
288     '''
289
290     def __init__(self, ctx, cmd, interpreter):
291         self.cmd = cmd
292         tokens = self.tokenize(cmd)
293         symbol = tokens.pop(0)
294         parts = symbol.split('.', 2)
295         namespace = parts[0]
296         if not interpreter.namespace:
297             return None
298         if not namespace == interpreter.namespace:
299             return None
300         if tokens:
301             args = tokens[:]
302             verb = args.pop(0)
303         else:
304             args = None
305             verb = None
306         if not verb:
307             verb = interpreter.default_verb
308         func = self.parse_verb(interpreter, verb)
309         # create Context object
310         currency = interpreter.currency
311         # XXX deprecate
312         ctx.res.market.cmd = cmd
313         ctx.res.market.currency = currency
314         ctx.res.market.symbol = symbol
315         ctx.res.market.namespace = namespace
316         ctx.res.market.verb = verb
317         ctx.res.market.args = args
318         # import pdb; pdb.set_trace()
319         kw = dict(ctx=ctx, interpreter=interpreter,
320             namespace=namespace, func=func,
321             symbol=symbol, verb=verb, args=args, currency=currency)
322         self.update(kw)
323
324     def __getattr__(self, name):
325         if self.has_key(name):
326             return self[name]
327         return getattr(self.ctx, name)
328
329     def parse_verb(self, interpreter, verb):
330         method = "cmd_%s" % verb
331         if not hasattr(interpreter, method):
332             raise SyntaxError, "unknown verb: %s: %s: %s" % (
333                 verb, self.cmd, interpreter)
334         return getattr(interpreter, method)
335
336     def tokenize(self, cmd):
337         # XXX handle quoted arguments
338         tokens = cmd.split()
339         return tokens
340
341     def __call__(self):
342         '''Execute command.'''
343         ctx = self.ctx
344         interpreter = self.interpreter
345         func = self.func
346         args = self.args
347         start = time.time()
348         interpreter.ck_accounts(ctx)
349         # call any pre-execution method
350         if hasattr(interpreter, "pre_cmd"):
351             getattr(interpreter, "pre_cmd")(self)
352         # call the cmd_* method
353         if args:
354             res = func(self, *args)
355         else:
356             res = func(self)
357         # call any post-execution method
358         if hasattr(interpreter, "post_cmd"):
359             getattr(interpreter, "post_cmd")(self)
360         end = time.time()
361         interpreter.query_timer(self, start, end)
362         # XXX deprecate
363         return res
364
365 class Timer(DbObject):
366     '''a set of simple, named, persistent timers'''
367
368     VERSION = 1
369     TABLE = Table('timer', key=('name'))[
370                 Column('name'),
371                 Column('tick', type='numeric'),
372             ]
373     NAME = TABLE.name
374     COLUMNS = [ c.name for c in TABLE.columns ]
375
376     def _set_defaults(self):
377         self.chattr(tick=0)
378
379     def control(cls, env, db, name):
380         timer = cls.selectone(env, db, name=name)
381         if not timer:
382             timer = cls(env, db, name=name, tick=0)
383             timer = timer.insert()
384         return timer
385     control = classmethod(control)
386
387     def expired(self, period):
388         if time.time() > self.tick + float(period):
389             return True
390         return False
391
392     def reset(self):
393         self.update(tick=time.time())
394
395
396 class InitCommand(InitDB):
397     implements(IEnvironmentSetupParticipant)
398     dbobjects = (Timer,)
399
Note: See TracBrowser for help on using the browser.