portfolio_item.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. # -*- coding: utf-8 -*-
  2. import sys, traceback
  3. import logging
  4. import json
  5. import math
  6. import time, datetime
  7. import copy
  8. from optparse import OptionParser
  9. from time import sleep
  10. from misc2.helpers import ContractHelper
  11. from finopt.instrument import Symbol, Option
  12. from rethink.option_chain import OptionsChain
  13. from rethink.tick_datastore import TickDataStore
  14. from numpy import average
  15. from rethink.table_model import AbstractTableModel
  16. class PortfolioRules():
  17. rule_map = {
  18. 'symbol': {'HSI' : 'FUT', 'MHI' : 'FUT', 'QQQ' : 'STK'},
  19. 'expiry': {'HSI' : 'same_month', 'MHI': 'same_month', 'STK': 'leave_blank'},
  20. 'option_structure': {
  21. 'HSI': {'spd_size': 200, 'multiplier': 50.0, 'rate': 0.0012, 'div': 0, 'trade_vol':0.15},
  22. 'MHI': {'spd_size': 200, 'multiplier': 10, 'rate': 0.0012, 'div': 0, 'trade_vol':0.15}
  23. },
  24. 'exchange': {'HSI': 'HKFE', 'MHI': 'HKFE'},
  25. 'interested_position_types': {'symbol': ['HSI', 'MHI'], 'instrument_type': ['OPT', 'FUT']}
  26. }
  27. class PortfolioItem():
  28. """
  29. """
  30. POSITION = 7001
  31. AVERAGE_COST = 7002
  32. POSITION_DELTA = 7003
  33. POSITION_THETA = 7004
  34. GAMMA_PERCENT = 7009
  35. UNREAL_PL = 7005
  36. PERCENT_GAIN_LOSS = 7006
  37. AVERAGE_PRICE = 7007
  38. MARKET_VALUE = 7008
  39. def __init__(self, account, contract_key, position, average_cost):
  40. self.contract_key = contract_key
  41. self.account_id = account
  42. self.port_fields = {PortfolioItem.POSITION: position,
  43. PortfolioItem.AVERAGE_COST: average_cost,
  44. PortfolioItem.POSITION_DELTA: float('nan'),
  45. PortfolioItem.POSITION_THETA: float('nan'),
  46. PortfolioItem.UNREAL_PL: float('nan'),
  47. PortfolioItem.PERCENT_GAIN_LOSS: float('nan'),
  48. PortfolioItem.AVERAGE_PRICE: float('nan'),
  49. PortfolioItem.MARKET_VALUE: float('nan')
  50. }
  51. contract = ContractHelper.makeContractfromRedisKeyEx(contract_key)
  52. if contract.m_secType == 'OPT':
  53. self.instrument = Option(contract)
  54. else:
  55. self.instrument = Symbol(contract)
  56. def set_port_field(self, id, value):
  57. self.port_fields[id] = value
  58. def get_port_field(self, id):
  59. try:
  60. return self.port_fields[id]
  61. except:
  62. return None
  63. def get_port_fields(self):
  64. return self.port_fields
  65. def get_contract_key(self):
  66. return self.contract_key
  67. def get_right(self):
  68. return self.instrument.get_contract().m_right
  69. def get_symbol_id(self):
  70. return self.instrument.get_contract().m_symbol
  71. def get_expiry(self):
  72. return self.instrument.get_contract().m_expiry
  73. def get_strike(self):
  74. return self.instrument.get_contract().m_strike
  75. def get_quantity(self):
  76. return self.port_fields[PortfolioItem.POSITION]
  77. def get_average_cost(self):
  78. return self.port_fields[PortfolioItem.AVERAGE_COST]
  79. def get_market_value(self):
  80. return self.port_fields[PortfolioItem.MARKET_VALUE]
  81. def get_instrument(self):
  82. return self.instrument
  83. def get_instrument_type(self):
  84. return self.instrument.get_contract().m_secType
  85. def get_account(self):
  86. return self.account_id
  87. def calculate_pl(self, contract_key):
  88. #logging.info('PortfolioItem:calculate_pl. %s' % self.dump())
  89. '''
  90. POSITION = 7001
  91. AVERAGE_COST = 7002
  92. POSITION_DELTA = 7003
  93. POSITION_THETA = 7004
  94. UNREAL_PL = 7005
  95. PERCENT_GAIN_LOSS = 7006
  96. AVERAGE_PRICE = 7007
  97. MARKET_VALUE = 7008
  98. '''
  99. try:
  100. assert contract_key == self.contract_key
  101. spot_px = self.instrument.get_tick_value(4)
  102. if self.get_instrument_type() == 'OPT':
  103. #spot_px = self.instrument.get_tick_value(4)
  104. multiplier = PortfolioRules.rule_map['option_structure'][self.get_symbol_id()]['multiplier']
  105. pos_delta = self.get_quantity() * self.instrument.get_tick_value(Option.DELTA) * multiplier
  106. pos_theta = self.get_quantity() * self.instrument.get_tick_value(Option.THETA) * multiplier
  107. gamma_percent = pos_delta * (1 + self.instrument.get_tick_value(Option.GAMMA))
  108. #(spot premium * multiplier - avgcost) * pos)
  109. try:
  110. unreal_pl = (spot_px * multiplier - self.get_average_cost()) * self.get_quantity()
  111. #print "%f %f %d" % (spot_px, self.get_average_cost(), multiplier)
  112. percent_gain_loss = (1 - spot_px / (self.get_average_cost() / multiplier)) * 100 \
  113. if self.get_quantity() < 0 else \
  114. (spot_px - self.get_average_cost() / multiplier) / (self.get_average_cost() / multiplier) * 100
  115. average_px = self.get_average_cost() / multiplier
  116. except ZeroDivisionError, TypeError:
  117. # caught error for cases where get_average_cost and quantity may be None
  118. unreal_pl = float('nan')
  119. percent_gain_loss = float('nan')
  120. average_px = float('nan')
  121. else:
  122. multiplier = PortfolioRules.rule_map['option_structure'][self.get_symbol_id()]['multiplier']
  123. pos_delta = self.get_quantity() * 1.0 * multiplier
  124. pos_theta = 0
  125. gamma_percent = 0
  126. # (S - X) * pos * multiplier
  127. unreal_pl = (spot_px * multiplier - self.get_average_cost() ) * self.get_quantity()
  128. #sign = abs(self.get_quantity()) / self.get_quantity()
  129. percent_gain_loss = unreal_pl / self.get_average_cost() * 100
  130. average_px = self.get_average_cost() / multiplier
  131. self.set_port_field(PortfolioItem.POSITION_DELTA, pos_delta)
  132. self.set_port_field(PortfolioItem.POSITION_THETA, pos_theta)
  133. self.set_port_field(PortfolioItem.GAMMA_PERCENT, gamma_percent)
  134. self.set_port_field(PortfolioItem.UNREAL_PL, unreal_pl)
  135. self.set_port_field(PortfolioItem.AVERAGE_PRICE, average_px)
  136. self.set_port_field(PortfolioItem.PERCENT_GAIN_LOSS, percent_gain_loss)
  137. except Exception, err:
  138. logging.error(traceback.format_exc())
  139. #logging.info('PortfolioItem:calculate_pl. %s' % self.dump())
  140. def update_position(self, position, average_cost, extra_info):
  141. self.set_port_field(PortfolioItem.POSITION, position)
  142. self.set_port_field(PortfolioItem.AVERAGE_COST, average_cost)
  143. if extra_info:
  144. self.set_port_field(PortfolioItem.MARKET_VALUE, extra_info['market_value'])
  145. def dump(self):
  146. s= ", ".join('[%s:%8.2f]' % (k, v) for k,v in self.port_fields.iteritems())
  147. return 'PortfolioItem contents: %s %s %s' % (self.contract_key, self.account_id, s)
  148. class Portfolio(AbstractTableModel):
  149. '''
  150. portfolio :
  151. {
  152. 'port_items': {<contract_key>, PortItem},
  153. 'opt_chains': {<oc_id>: option_chain},
  154. 'g_table':{'rows':{...} , 'cols':{...},
  155. 'header':{...},
  156. 'row_index': <curr_index>,
  157. 'ckey_to_row_index':{<contract_key>: <row_id>},
  158. 'row_to_ckey_index':{<row_id>: <contract_key>},
  159. 'port_v': port_v
  160. }
  161. '''
  162. TOTAL_DELTA = 9000
  163. TOTAL_DELTA_F = 9001
  164. TOTAL_DELTA_C = 9002
  165. TOTAL_DELTA_P = 9003
  166. TOTAL_THETA = 9010
  167. TOTAL_THETA_C = 9012
  168. TOTAL_THETA_P = 9013
  169. TOTAL_GAMMA_PERCENT = 9020
  170. NUM_CALLS = 9031
  171. NUM_PUTS = 9032
  172. TOTAL_GAIN_LOSS = 9040
  173. def __init__(self, account):
  174. self.account = account
  175. self.create_empty_portfolio()
  176. AbstractTableModel.__init__(self)
  177. def get_object_name(self):
  178. return json.dumps({'account': self.account, 'id': id(), 'class': self.__class__.__name__})
  179. def is_contract_in_portfolio(self, contract_key):
  180. return self.get_portfolio_port_item(contract_key)
  181. def get_portfolio_port_item(self, contract_key):
  182. try:
  183. return self.port['port_items'][contract_key]
  184. except KeyError:
  185. return None
  186. def get_portfolio_port_items(self):
  187. return self.port['port_items']
  188. def create_empty_portfolio(self):
  189. self.port = {}
  190. self.port['port_items']= {}
  191. self.port['opt_chains']= {}
  192. self.port['g_table']= {'row_index': 0, 'ckey_to_row_index': {}, 'row_to_ckey_index': {}}
  193. self.init_table()
  194. return self.port
  195. def set_portfolio_port_item(self, contract_key, port_item):
  196. self.port['port_items'][contract_key] = port_item
  197. '''
  198. update the gtable contract_key to row number index
  199. '''
  200. self.update_ckey_row_xref(contract_key, port_item)
  201. def is_oc_in_portfolio(self, oc_id):
  202. try:
  203. return self.port['opt_chains'][oc_id]
  204. except KeyError:
  205. return None
  206. def get_option_chain(self, oc_id):
  207. return self.is_oc_in_portfolio(oc_id)
  208. def set_option_chain(self, oc_id, oc):
  209. self.port['opt_chains'][oc_id] = oc
  210. def get_option_chains(self):
  211. return self.port['opt_chains']
  212. def calculate_item_pl(self, contract_key):
  213. self.port['port_items'][contract_key].calculate_pl(contract_key)
  214. def calculate_port_pl(self):
  215. p1_items = filter(lambda x: x[1].get_symbol_id() in PortfolioRules.rule_map['interested_position_types']['symbol'], self.port['port_items'].items())
  216. p2_items = filter(lambda x: x[1].get_instrument_type() in PortfolioRules.rule_map['interested_position_types']['instrument_type'], p1_items)
  217. port_v = {
  218. Portfolio.TOTAL_DELTA : 0.0,
  219. Portfolio.TOTAL_DELTA_F : 0.0,
  220. Portfolio.TOTAL_DELTA_C : 0.0,
  221. Portfolio.TOTAL_DELTA_P : 0.0,
  222. Portfolio.TOTAL_THETA : 0.0,
  223. Portfolio.TOTAL_THETA_C : 0.0,
  224. Portfolio.TOTAL_THETA_P : 0.0,
  225. Portfolio.TOTAL_GAMMA_PERCENT : 0.0,
  226. Portfolio.NUM_CALLS : 0,
  227. Portfolio.NUM_PUTS : 0,
  228. Portfolio.TOTAL_GAIN_LOSS : 0.0
  229. }
  230. def cal_port(x_tuple):
  231. x = x_tuple[1]
  232. if x.get_right() == 'C':
  233. port_v[Portfolio.TOTAL_DELTA_C] += x.get_port_field(PortfolioItem.POSITION_DELTA)
  234. port_v[Portfolio.TOTAL_THETA_C] += x.get_port_field(PortfolioItem.POSITION_THETA)
  235. ##
  236. # hard coded logic
  237. #
  238. port_v[Portfolio.NUM_CALLS] += (
  239. x.get_quantity() * PortfolioRules.rule_map['option_structure'][x.get_symbol_id()]['multiplier'] / 50)
  240. elif x.get_right() == 'P':
  241. port_v[Portfolio.TOTAL_DELTA_P] += x.get_port_field(PortfolioItem.POSITION_DELTA)
  242. port_v[Portfolio.TOTAL_THETA_P] += x.get_port_field(PortfolioItem.POSITION_THETA)
  243. port_v[Portfolio.NUM_PUTS] += (
  244. x.get_quantity() * PortfolioRules.rule_map['option_structure'][x.get_symbol_id()]['multiplier'] / 50)
  245. elif x.get_instrument_type() == 'FUT':
  246. port_v[Portfolio.TOTAL_DELTA_F] += x.get_port_field(PortfolioItem.POSITION_DELTA)
  247. port_v[Portfolio.TOTAL_DELTA] += x.get_port_field(PortfolioItem.POSITION_DELTA)
  248. port_v[Portfolio.TOTAL_THETA] += x.get_port_field(PortfolioItem.POSITION_THETA)
  249. port_v[Portfolio.TOTAL_GAIN_LOSS] += x.get_port_field(PortfolioItem.UNREAL_PL)
  250. try:
  251. port_v[Portfolio.TOTAL_GAMMA_PERCENT] += x.get_port_field(PortfolioItem.GAMMA_PERCENT)
  252. except:
  253. logging.error('Portfolio:calculate_port_pl. Error calcuting gamma percent %s' % traceback.format_exc())
  254. map(cal_port, p2_items)
  255. self.port['port_v'] = port_v
  256. return self.port['port_v']
  257. def dump_portfolio(self):
  258. #<account_id>: {'port_items': {<contract_key>, instrument}, 'opt_chains': {<oc_id>: option_chain}}
  259. def print_port_items(x):
  260. return '[%s]: %s %s' % (x[0], ', '.join('%s: %s' % (k,str(v)) for k, v in x[1].get_port_fields().iteritems()),
  261. ', '.join('%s: %s' % (k,str(v)) for k, v in x[1].get_instrument().get_tick_values().iteritems()))
  262. p_items = map(print_port_items, [x for x in self.port['port_items'].iteritems()])
  263. logging.info('PortfolioMonitor:dump_portfolio %s' % ('\n'.join(p_items)))
  264. return '\n'.join(p_items)
  265. '''
  266. implement AbstractTableModel methods and other routines
  267. '''
  268. def init_table(self):
  269. self.port['g_table']['header'] = [('symbol', 'Symbol', 'string'), ('right', 'Right', 'string'), ('avgcost', 'Avg Cost', 'number'), ('market_value', 'Market Value', 'number'),
  270. ('avgpx', 'Avg Price', 'number'), ('spotpx', 'Spot Price', 'number'), ('pos', 'Quantity', 'number'),
  271. ('delta', 'Delta', 'number'), ('theta', 'Theta', 'number'), ('gamma', 'Gamma', 'number'),
  272. ('pos_delta', 'P. Delta', 'number'), ('pos_theta', 'P. Theta', 'number'), ('gamma_percent', 'P. Gamma', 'number'),
  273. ('unreal_pl', 'Unreal P/L', 'number'), ('percent_gain_loss', '% gain/loss', 'number'),
  274. ('symbolid', 'Sym Id', 'string')
  275. ]
  276. def update_ckey_row_xref(self, contract_key, port_item):
  277. # if port_item.get_symbol_id() in PortfolioRules.rule_map['interested_position_types']['symbol'] and \
  278. # port_item.get_instrument_type() in PortfolioRules.rule_map['interested_position_types']['instrument_type']:
  279. row_id = self.port['g_table']['row_index']
  280. self.port['g_table']['ckey_to_row_index'][contract_key] = row_id
  281. self.port['g_table']['row_to_ckey_index'][row_id] = contract_key
  282. self.port['g_table']['row_index'] += 1
  283. def ckey_to_row(self, contract_key):
  284. return self.port['g_table']['ckey_to_row_index'][contract_key]
  285. def get_column_count(self):
  286. return len(self.port['g_table']['header'])
  287. def get_row_count(self):
  288. p_items = [x for x in self.port['port_items'].iteritems()]
  289. # p1_items = filter(lambda x: x[1].get_symbol_id() in PortfolioRules.rule_map['interested_position_types']['symbol'], p_items)
  290. # p2_items = filter(lambda x: x[1].get_instrument_type() in PortfolioRules.rule_map['interested_position_types']['instrument_type'], p1_items)
  291. # return len(p2_items)
  292. return len(p_items)
  293. def get_column_name(self, col):
  294. return self.port['g_table']['header'][col][1]
  295. def get_column_id(self, col):
  296. return self.port['g_table']['header'][col][0]
  297. def get_value_at(self, row, col):
  298. # ckey = self.port['g_table']['row_to_ckey_index'][row]
  299. # p_item = self.port['port_items'][ckey]
  300. raise NotImplementedError
  301. def get_values_at(self, row):
  302. ckey = self.port['g_table']['row_to_ckey_index'][row]
  303. p_item = self.port['port_items'][ckey]
  304. return self.port_item_to_row_fields((None, p_item))
  305. def port_item_to_row_fields(self, x):
  306. def handle_NaN(n):
  307. # the function JSON.parse will fail at the javascript side if it encounters
  308. # a NaN value in the json string. Convert Nan to null to circumvent the issue
  309. try:
  310. return None if math.isnan(n) else n
  311. except:
  312. return None
  313. rf = [{'v': '%s-%s-%s' % (x[1].get_symbol_id(), x[1].get_expiry(), x[1].get_strike())},
  314. {'v': x[1].get_right()},
  315. {'v': handle_NaN(x[1].get_port_field(PortfolioItem.AVERAGE_COST))},
  316. {'v': handle_NaN(x[1].get_port_field(PortfolioItem.MARKET_VALUE))},
  317. {'v': handle_NaN(x[1].get_port_field(PortfolioItem.AVERAGE_PRICE))},
  318. {'v': handle_NaN(self.get_spot_px(x[1]))},
  319. {'v': x[1].get_quantity()},
  320. {'v': handle_NaN(x[1].get_instrument().get_tick_value(Option.DELTA))},
  321. {'v': handle_NaN(x[1].get_instrument().get_tick_value(Option.THETA))},
  322. {'v': handle_NaN(x[1].get_instrument().get_tick_value(Option.GAMMA))},
  323. {'v': handle_NaN(x[1].get_port_field(PortfolioItem.POSITION_DELTA))},
  324. {'v': handle_NaN(x[1].get_port_field(PortfolioItem.POSITION_THETA))},
  325. {'v': handle_NaN(x[1].get_port_field(PortfolioItem.GAMMA_PERCENT))},
  326. {'v': handle_NaN(x[1].get_port_field(PortfolioItem.UNREAL_PL))},
  327. {'v': handle_NaN(x[1].get_port_field(PortfolioItem.PERCENT_GAIN_LOSS))},
  328. {'v': x[1].get_symbol_id()}
  329. ]
  330. return rf
  331. def set_value_at(self, row, col, value):
  332. pass
  333. def get_spot_px(self, x):
  334. px = float('nan')
  335. if x.get_quantity() > 0:
  336. px= x.get_instrument().get_tick_value(Symbol.BID)
  337. elif x.get_quantity() < 0:
  338. px= x.get_instrument().get_tick_value(Symbol.ASK)
  339. if px == -1:
  340. return x.get_instrument().get_tick_value(Symbol.LAST)
  341. return px
  342. def get_JSON(self):
  343. dtj = {'cols':[], 'rows':[], 'ckey_to_row_index':{}}
  344. # header fields
  345. map(lambda hf: dtj['cols'].append({'id': hf[0], 'label': hf[1], 'type': hf[2]}), self.port['g_table']['header'])
  346. #p_items = sorted([x for x in self.port['port_items'].iteritems()])
  347. # create a list of port items tuples (contract_key, port_item) ordered by row_id
  348. # that is in the order when each items was created and inserted into the map
  349. # this ensures that the same sequence is replicated to the google datatable
  350. p_items = map(lambda x:(self.port['g_table']['row_to_ckey_index'][x],
  351. self.port['port_items'][ self.port['g_table']['row_to_ckey_index'][x] ]), range(self.port['g_table']['row_index']))
  352. #p1_items = filter(lambda x: x[1].get_symbol_id() in PortfolioRules.rule_map['interested_position_types']['symbol'], p_items)
  353. #p2_items = filter(lambda x: x[1].get_instrument_type() in PortfolioRules.rule_map['interested_position_types']['instrument_type'], p1_items)
  354. #map(lambda p: dtj['rows'].append({'c': self.port_item_to_row_fields(p)}), p2_items)
  355. map(lambda p: dtj['rows'].append({'c': self.port_item_to_row_fields(p)}), p_items)
  356. return json.dumps(dtj) #, indent=4)
  357. def dump_table_index_map(self):
  358. return '\n'.join('[%d]:%s' % (x[0], x[1]) for x in self.port['g_table']['row_to_ckey_index'].items())