| 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 |
|
|---|
| 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 |
|
|---|
| 28 |
Column('tail_id', type='integer'), |
|---|
| 29 |
Column('symbol'), |
|---|
| 30 |
Column('currency'), |
|---|
| 31 |
Column('type'), |
|---|
| 32 |
|
|---|
| 33 |
|
|---|
| 34 |
Column('status', type='integer'), |
|---|
| 35 |
|
|---|
| 36 |
Column('bid_pending', type='numeric'), |
|---|
| 37 |
Column('ask_pending', type='numeric'), |
|---|
| 38 |
|
|---|
| 39 |
Column('total_fill', type='numeric'), |
|---|
| 40 |
|
|---|
| 41 |
Column('limit_price', type='numeric'), |
|---|
| 42 |
Column('stop_price', type='numeric'), |
|---|
| 43 |
|
|---|
| 44 |
Column('expires', type='numeric'), |
|---|
| 45 |
|
|---|
| 46 |
Column('owner'), |
|---|
| 47 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 131 |
|
|---|
| 132 |
|
|---|
| 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 |
|
|---|
| 152 |
|
|---|
| 153 |
|
|---|
| 154 |
|
|---|
| 155 |
|
|---|
| 156 |
|
|---|
| 157 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 185 |
|
|---|
| 186 |
if seller == agent: |
|---|
| 187 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 211 |
legs.append(ledger.mkleg(db, entity=seller, |
|---|
| 212 |
account='available', debit=total, symbol=currency)) |
|---|
| 213 |
|
|---|
| 214 |
|
|---|
| 215 |
|
|---|
| 216 |
|
|---|
| 217 |
|
|---|
| 218 |
if bid.limit_price: |
|---|
| 219 |
|
|---|
| 220 |
accounts = ('reserve', 'available') |
|---|
| 221 |
else: |
|---|
| 222 |
|
|---|
| 223 |
|
|---|
| 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 |
|
|---|
| 235 |
assert not ask.limit_price |
|---|
| 236 |
ask.cancel(db, ledger, agent) |
|---|
| 237 |
else: |
|---|
| 238 |
legs.append(leg) |
|---|
| 239 |
|
|---|
| 240 |
|
|---|
| 241 |
xid = ledger.post(db, agent, legs, memo=memo) |
|---|
| 242 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 404 |
VERSION = 1 |
|---|
| 405 |
TABLE = Table('order_detail', key=('id'))[ |
|---|
| 406 |
Column('id', auto_increment=True), |
|---|
| 407 |
|
|---|
| 408 |
Column('head_id', type='integer'), |
|---|
| 409 |
|
|---|
| 410 |
Column('xid', type='integer'), |
|---|
| 411 |
|
|---|
| 412 |
Column('symbol'), |
|---|
| 413 |
|
|---|
| 414 |
Column('currency'), |
|---|
| 415 |
|
|---|
| 416 |
Column('action', type='integer'), |
|---|
| 417 |
|
|---|
| 418 |
|
|---|
| 419 |
Column('bid', type='numeric'), |
|---|
| 420 |
|
|---|
| 421 |
|
|---|
| 422 |
Column('ask', type='numeric'), |
|---|
| 423 |
|
|---|
| 424 |
Column('fill', type='numeric'), |
|---|
| 425 |
|
|---|
| 426 |
Column('fill_price', type='numeric'), |
|---|
| 427 |
|
|---|
| 428 |
Column('limit_price', type='numeric'), |
|---|
| 429 |
Column('stop_price', type='numeric'), |
|---|
| 430 |
|
|---|
| 431 |
Column('expires', type='numeric'), |
|---|
| 432 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 488 |
|
|---|
| 489 |
|
|---|
| 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 |
|
|---|
| 547 |
|
|---|
| 548 |
|
|---|
| 549 |
|
|---|
| 550 |
|
|---|
| 551 |
|
|---|
| 552 |
|
|---|
| 553 |
|
|---|
| 554 |
|
|---|
| 555 |
|
|---|
| 556 |
basket = self.pop('basket', None) |
|---|
| 557 |
|
|---|
| 558 |
|
|---|
| 559 |
|
|---|
| 560 |
|
|---|
| 561 |
new = self |
|---|
| 562 |
price_delta = (new.limit_price or 0) - (head.limit_price or 0) |
|---|
| 563 |
|
|---|
| 564 |
|
|---|
| 565 |
|
|---|
| 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 |
|
|---|
| 572 |
|
|---|
| 573 |
|
|---|
| 574 |
|
|---|
| 575 |
|
|---|
| 576 |
if delta and not new.fill: |
|---|
| 577 |
|
|---|
| 578 |
|
|---|
| 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 |
|
|---|
| 588 |
|
|---|
| 589 |
|
|---|
| 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 |
|
|---|
| 599 |
|
|---|
| 600 |
|
|---|
| 601 |
super(OrderDetail, self).insert(db, cursor=cursor); |
|---|
| 602 |
id = db.get_last_id(cursor, self.NAME) |
|---|
| 603 |
self.chattr(id=id) |
|---|
| 604 |
|
|---|
| 605 |
|
|---|
| 606 |
head = head.refresh() |
|---|
| 607 |
self._head = None |
|---|
| 608 |
|
|---|
| 609 |
return head |
|---|
| 610 |
|
|---|
| 611 |
|
|---|
| 612 |
|
|---|
| 613 |
|
|---|
| 614 |
|
|---|
| 615 |
|
|---|
| 616 |
|
|---|
| 617 |
|
|---|
| 618 |
|
|---|
| 619 |
|
|---|
| 620 |
|
|---|
| 621 |
|
|---|
| 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 |
|
|---|
| 631 |
|
|---|
| 632 |
|
|---|
| 633 |
|
|---|
| 634 |
working_basket = [symbol] |
|---|
| 635 |
while True: |
|---|
| 636 |
legs = [] |
|---|
| 637 |
try: |
|---|
| 638 |
for symbol in working_basket: |
|---|
| 639 |
if amount > 0: |
|---|
| 640 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|---|
| 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 |
|
|---|
| 683 |
new = OrderDetail(env, db, |
|---|
| 684 |
id=None, head_id=head.id, |
|---|
| 685 |
**attr) |
|---|
| 686 |
|
|---|
| 687 |
|
|---|
| 688 |
|
|---|
| 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 |
|---|
| 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 |
|
|---|
| 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 |
|
|---|
| 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 |
|
|---|