class User(Document): """ 跌零用户 """ class Meta: idf = IDFormatter('{mobile}') idx1 = Index('mobile', unique=True) idx2 = Index('username') mobile = StringField(required=True) username = StringField(required=True) password = StringField(required=True) paid = FloatField(default=0) total_money = FloatField(default=0) total_capital = FloatField(default=0) total_profit = FloatField(default=0) _is_admin = BooleanField(default=False) def get_id(self): return self._id def is_active(self): return True def is_anonymous(self): return False def is_authenticated(self): return True def is_admin(self): return self._is_admin
class Account(Document): """ 抢单用户的账号 """ class Meta: idf = IDFormatter('{user}_{exchange}_{login_name}') user = StringField(required=True) exchange = StringField(required=True) login_name = StringField(required=True) login_password = StringField(required=True) money_password = StringField() bank_password = StringField() collections = ListField(StringField, default=[]) position = ListField(EmbeddedField(MyPosition)) orders = ListField(EmbeddedField(MyOrder)) order_status = ListField(EmbeddedField(MyStatus)) money = FloatField() capital = FloatField() profit = FloatField() earned = FloatField() # 交易收入 lost = FloatField() # 交易亏损 @property def mobile(self): user = User.cached(60).query_one({'_id': self.user}) return user.mobile @property def username(self): user = User.cached(60).query_one({'_id': self.user}) return user.username
class MyPosition(EmbeddedDocument): """ 持仓 """ name = StringField(required=True) symbol = StringField(required=True) average_price = FloatField(required=True) quantity = IntField(required=True) price = FloatField(required=True) sellable = IntField() profit = FloatField()
class Order(EmbeddedDocument): """ 成交订单汇总 """ type_ = StringField(required=True) name = StringField(required=True) symbol = StringField(required=True) price = FloatField(required=True) # 成本价 current_price = FloatField(required=True) # 成交价 quantity = IntField(required=True) commision = FloatField(required=True) profit = FloatField(required=True)
class Exchange(Document): """ 交易所 """ class Meta: idf = IDFormatter('{name}') name = StringField(required=True) num_users = IntField() num_accounts = IntField() position = ListField(EmbeddedField(MyPosition)) orders = ListField(EmbeddedField(MyOrder)) order_status = ListField(EmbeddedField(MyStatus)) money = FloatField() capital = FloatField() profit = FloatField() earned = FloatField() # 交易收入 lost = FloatField() # 交易亏损
class DailyTrading(Document): """ 当日账号汇总 """ class Meta: idf = IDFormatter('{account}_{date}') idx1 = Index(['account', 'date'], unique=True) date = DateTimeField(required=True) account = StringField(required=True) position = ListField(EmbeddedField(MyPosition)) orders = ListField(EmbeddedField(MyOrder)) order_status = ListField(EmbeddedField(MyStatus)) profit = FloatField() money = FloatField() capital = FloatField() earned = FloatField() # 交易收入 lost = FloatField() # 交易亏损
class Transaction(Document): """ 用户交易信息 """ class Meta: idx1 = Index(['user', 'type_', 'exchange', 'symbol']) idx2 = Index(['user', 'operated_at']) exchange = StringField(required=True) # 交易所ID(简称) symbol = StringField(required=True) # 交易代码 user = StringField(required=True) type_ = StringField(required=True) # buy/sell price = FloatField(required=True) quantity = IntField(required=True) operated_at = DateTimeField(created=True) @classmethod def user_total_transactions(cls, user): """ 用户总操作次数 """ return cls.count({'user': user}) @classmethod def user_recent_transactions(cls, user, offset=0, limit=None): """ 用户最近的操作 """ qs = cls.query({'user': user}, sort=[('operated_at', -1)]) if offset: qs.skip(offset) if limit: qs.limit(limit) return list(qs)
class MyPosition(EmbeddedDocument): """ 持仓汇总 """ name = StringField(required=True) symbol = StringField(required=True) average_price = FloatField(required=True) quantity = IntField(required=True) price = FloatField(required=True) sellable = IntField() profit = FloatField() @property def increase(self): if self.price > 0: return '{:4.2f}%'.format( (self.price / self.average_price - 1) * 100) else: return '0%'
class MyStatus(EmbeddedDocument): """ 挂单情况 """ order = StringField(required=True) order_at = StringField(required=True) type_ = StringField(required=True) name = StringField(required=True) symbol = StringField(required=True) price = FloatField(required=True) quantity = IntField(required=True) pending_quantity = IntField(required=True) status = StringField(required=True)
class Summary(Document): """ 各种组合的汇总信息 总资金/总持仓/总浮盈 """ class Meta: idf = IDFormatter('{exchange}_{user}_{collection}') idx1 = Index('exchange') idx2 = Index('user') idx3 = Index('collection') exchange = StringField(default='', required=True) user = StringField(default='', required=True) collection = StringField(default='', required=True) position = ListField(EmbeddedField(MyPosition)) orders = ListField(EmbeddedField(MyOrder)) order_status = ListField(EmbeddedField(MyStatus)) money = FloatField() capital = FloatField() profit = FloatField() earned = FloatField() # 交易收入 lost = FloatField() # 交易亏损
class Collection(Document): class Meta: idf = IDFormatter('{exchange}_{symbol}') exchange = StringField(required=True) symbol = StringField(required=True) name = StringField(required=True) trade_day = IntField(required=True) # 现在是开始交易的第几个交易日? buy_price = FloatField(default=0, required=True) # 平均买入价 quantity = IntField(default=0, required=True) # 总持有数量 accounts = ListField(StringField) # 持有这个品种的账号 users = ListField(StringField) # 持有这个品种的用户 updated_at = DateTimeField(modified=True) @property def num_users(self): return len(self.users) @property def num_accounts(self): return len(self.accounts)
class User(Document): """ 抢单用户 """ class Meta: idf = IDFormatter('{mobile}') idx1 = Index('mobile', unique=True) idx2 = Index('username') mobile = StringField(required=True) username = StringField(required=True) password = StringField(required=True) owning = FloatField(default=0) # 欠款 paid = FloatField(default=0) # 已结清 num_exchanges = IntField() num_accounts = IntField() profit = FloatField() money = FloatField() capital = FloatField() earned = FloatField() # 交易收入 lost = FloatField() # 交易亏损 position = ListField(EmbeddedField(MyPosition)) orders = ListField(EmbeddedField(MyOrder)) order_status = ListField(EmbeddedField(MyStatus)) _is_admin = BooleanField() def get_id(self): return self._id def is_active(self): return True def is_anonymous(self): return False def is_authenticated(self): return True def is_admin(self): return self._is_admin
class Position(Document): """ 用户持仓信息 """ class Meta: idx1 = Index(['user', 'exchange', 'symbol'], unique=True) exchange = StringField(required=True) # 交易所ID(简称) symbol = StringField(required=True) # 交易代码 user = StringField(required=True) quantity = FloatField(required=True, default=0) @classmethod def num_exchanges(cls, user): """ 用户持有多少个交易所的持仓 """ return len( set(p.exchange for p in cls.query({'user': user}, {'exchange': 1}))) @classmethod def num_collections(cls, user): """ 用户持有多少不同的藏品 """ return len( list(cls.query({ 'user': user, 'quantity': { '$gt': 0 } }, {'_id': 1}))) @classmethod def num_sold(cls, user): """ 用户已卖出过多少个藏品 """ return len( set('{exchange}_{symbol}'.format(**t) for t in Transaction.find({ 'user': user, 'type_': 'sell' }, { 'exchange': 1, 'symbol': 1 }))) @classmethod def user_position(cls, user): """ 目前持仓概况, cached """ cachekey = 'pcache_{}'.format(user) if not hasattr(cls, cachekey): setattr(cls, cachekey, {}) now = time.time() cache = getattr(cls, cachekey) if 'position' not in cache or \ cache.get('time', now) < now - 5: cache['position'] = cls._user_position(user) cache['time'] = time.time() return copy.deepcopy(cache['position']) @classmethod def _user_position(cls, user): """ 目前持仓概况 """ collections = {} for p in cls.query({'user': user}): pair = (p.exchange, p.symbol) collections.setdefault(pair, 0) collections[pair] += p.quantity buy_info = {} for t in Transaction.query({ 'user': user, 'type_': 'buy' }, sort=[('operated_at', 1)]): pair = (t.exchange, t.symbol) if pair in collections: buy_info.setdefault(pair, [0, 0, None]) buy_info[pair][0] += t.quantity buy_info[pair][1] += t.quantity * t.price if not buy_info[pair][2]: buy_info[pair][2] = t.operated_at sell_info = {} for t in Transaction.query({ 'user': user, 'type_': 'sell' }, sort=[('operated_at', -1)]): pair = (t.exchange, t.symbol) if pair in collections: sell_info.setdefault(pair, [0, 0, None]) sell_info[pair][0] += t.quantity sell_info[pair][1] += t.quantity * t.price if not sell_info[pair][2]: sell_info[pair][2] = t.operated_at position = [] for pair, quantity_amount in buy_info.items(): exchange, symbol = pair quantity, amount, first_buy_at = quantity_amount if quantity: avg_buy_price = amount / quantity realized_profit = 0 last_sell_at = datetime.utcnow() if pair in sell_info: quantity2, amount2, last_sell_at = sell_info[pair] quantity -= quantity2 realized_profit = amount2 - avg_buy_price * quantity2 if quantity != collections[pair]: cls.update_one( { 'exchange': pair[0], 'symbol': pair[1], 'user': user }, {'$set': { 'quantity': quantity }}) latest_price = Quote.latest_price(exchange, symbol) increase = Quote.increase(exchange, symbol) if not latest_price: latest_price = avg_buy_price unrealized_profit = (latest_price - avg_buy_price) * quantity past_days = max(1, (last_sell_at - first_buy_at).days) annual_profit = (365 / past_days) * \ (unrealized_profit + realized_profit) position.append({ 'exchange': exchange, 'symbol': symbol, 'name': Collection.get_name(exchange, symbol), 'avg_buy_price': avg_buy_price, 'quantity': quantity, 'latest_price': latest_price, 'increase': increase, 'total_increase': latest_price / avg_buy_price - 1 if avg_buy_price > 0 else 100, 'realized_profit': realized_profit, 'unrealized_profit': unrealized_profit, 'annual_profit': annual_profit, }) return sorted(position, key=lambda x: x['exchange']) @classmethod def average_increase(cls, user): """ 平均涨幅 """ position = [p for p in cls.user_position(user) if p['quantity'] > 0] if len(position): return sum(p['total_increase'] for p in position) / len(position) @classmethod def market_value(cls, user): """ 持仓总市值 """ position = cls.user_position(user) return sum(p['latest_price'] * p['quantity'] for p in position) @classmethod def unrealized_profit(cls, user): """ 未实现收益(浮盈) """ return sum(p['unrealized_profit'] for p in cls.user_position(user)) @classmethod def realized_profit(cls, user): """ 已实现收益 """ return sum(p['realized_profit'] for p in cls.user_position(user)) @classmethod def annual_profit(cls, user): """ 年化收益 """ return sum(p['annual_profit'] for p in cls.user_position(user)) @classmethod def do_op(cls, t, reverse=False): c = cls.query_one({ 'user': t.user, 'exchange': t.exchange, 'symbol': t.symbol }) if not c: if reverse: return False elif t.type_ == 'buy': c = cls({ 'user': t.user, 'exchange': t.exchange, 'symbol': t.symbol, 'quantity': t.quantity }) else: return False elif t.type_ == 'buy': c.quantity += t.quantity elif t.type_ == 'sell': c.quantity -= t.quantity if c.quantity >= 0: c.upsert() return True else: return False
class Quote(Document): """ 交易行情信息 """ class Meta: idf = IDFormatter('{exchange}_{symbol}_{quote_type}_{quote_at}') idx1 = Index(['exchange', 'symbol', 'quote_type', 'quote_at'], unique=True) idx2 = Index(['exchange', 'quote_at']) exchange = StringField(required=True) # 交易所ID(简称) symbol = StringField(required=True) # 交易代码 # 1m/5m/15m/30m/1h/4h/1d/1w/1m quote_type = StringField(required=True) quote_at = DateTimeField(required=True) # 交易时间 lclose = FloatField() # 上次收盘价 open_ = FloatField(required=True) # 周期开盘价 high = FloatField(required=True) # 周期最高价 low = FloatField(required=True) # 周期最低价 close = FloatField(required=True) # 周期收盘价 mean = FloatField() # 周期均价 volume = IntField(required=True) # 周期成交量 amount = FloatField(required=True) # 周期成交额 @classmethod def latest_price(cls, exchange, symbol): """ 获得品种的最新成交价 从日线数据中取就可以了, 实时交易价格也会保存在日线中 """ today = datetime.utcnow() + timedelta(hours=8) today = today.replace(hour=0, minute=0, second=0, microsecond=0) q = cls.cached(300).query_one( { 'exchange': exchange, 'symbol': symbol, 'quote_type': '1d', 'quote_at': { '$lte': today } }, {'close': 1}, sort=[('quote_at', -1)]) if q: return q.close @classmethod def increase(cls, exchange, symbol): """ 获得品种的今日涨幅 """ now = datetime.utcnow() + timedelta(hours=8) if now.hour < 9 and now.minute < 30: now -= timedelta(days=1) date = now.replace(hour=0, minute=0, second=0, microsecond=0) q = cls.cached(300).query_one( { 'exchange': exchange, 'symbol': symbol, 'quote_type': '1d', 'quote_at': date }, { 'close': 1, 'lclose': 1 }) if q and q.lclose: return q.close / q.lclose - 1
class MyMoney(EmbeddedDocument): """ 资金 """ usable = FloatField(required=True) withdrawable = FloatField(required=True)
class Collection(Document): """ 收藏品: Stamp/Coin/Card """ class Meta: idf = IDFormatter('{exchange}_{symbol}') idx1 = Index(['exchange', 'symbol'], unique=True) idx2 = Index('from_url') from_url = StringField() # 来自哪个公告 exchange = StringField(required=True) # 交易所ID(简称) symbol = StringField(required=True) # 交易代码 name = StringField() # 交易名 type_ = StringField(default="邮票") # "邮票"/"钱币"/"卡片" status = StringField(default="申购中") # "申购中"/"已上市" issuer = StringField() # 发行机构 texture = StringField() # 材质 price_forsale = FloatField() # 挂牌参考价 quantity_all = IntField() # 挂牌总数量 quantity_forsale = IntField() # 限售总数 offer_fee = FloatField(default=0.001) # 申购手续费 offer_quantity = IntField() # 供申购数量 * offer_price = FloatField() # 申购价格 * offer_accmax = IntField() # 单账户最大中签数 * offer_overbuy = BooleanField() # 是否可超额申购 * offer_cash_ratio = FloatField() # 资金配售比例 * change_min = FloatField() # 最小价格变动单位 change_limit_1 = FloatField() # 首日涨跌幅 change_limit = FloatField() # 正常日涨跌幅 pickup_min = IntField() # 最小提货量 trade_limit = FloatField() # 单笔最大下单量 offers_at = DateTimeField() # 申购日 * draws_at = DateTimeField() # 抽签日 * trades_at = DateTimeField() # 上市交易日 * invest_mv = FloatField() # 申购市值(Market Value) invest_cash = FloatField() # 申购资金 * invest_cash_return_ratio = FloatField() # 资金中签率, 和上面那个二选一 * updated_at = DateTimeField(modified=True) @classmethod def get_name(cls, exchange, symbol): if not hasattr(cls, 'cache'): setattr(cls, 'cache', {}) pair = (exchange, symbol) if not cls.cache or cls.cache.get('time', time.time()) < 3600: cls.cache = {(c.exchange, c.symbol): c.name for c in cls.query({}, {'exchange': 1, 'symbol': 1, 'name': 1, '_id': 0})} cls.cache['time'] = time.time() return cls.cache.get(pair) @classmethod def search(cls, name_or_abbr, limit=100): # warm cache cls.get_name('', '') na = name_or_abbr.upper() pairs = [] exchanges = set() def add_name(name, pair): initials = py.get_initials(name, '') sna = set(na) sname = set(name) sinit = set(initials) if name.startswith(na) or initials.startswith(na): distance = min(max(len(sna - sname), len(sname - sna)), max(len(sna - sinit), len(sinit - sna))) pairs.append((distance, pair)) def add_exchange(name): initials = py.get_initials(name, '') sna = set(na) sname = set(name) sinit = set(initials) if name.startswith(na) or initials.startswith(na): distance = min(max(len(sna - sname), len(sname - sna)), max(len(sna - sinit), len(sinit - sna))) for pair in cls.cache.keys(): if pair[0] == name: pairs.append((distance, pair)) for pair in cls.cache.keys(): if pair == 'time': continue exchanges.add(pair[0]) name = cls.get_name(*pair) add_name(name, pair) for name in exchanges: add_exchange(name) return [p[1] for p in sorted(pairs)][:limit] @property def abbr(self): return py.get_initials(self.name, '') @property def offer_mv(self): """ 申购市值配额 """ if self.offer_quantity: return self.offer_quantity * self.offer_price * \ (1 - self.offer_cash_ratio) @property def offer_cash(self): """ 申购资金配额 """ try: return self.offer_quantity * self.offer_price * \ self.offer_cash_ratio except: pass @property def offer_max_invest(self): """ 最大申购资金 """ if self.offer_overbuy: return float('inf') else: return self.offer_mv + \ self.offer_fee * self.offer_accmax @property def invest_cash_real(self): """ 申购资金(含估算) """ if self.invest_cash: return self.invest_cash elif self.invest_cash_return_ratio and self.offer_cash: return self.offer_cash / self.invest_cash_return_ratio @property def result_ratio_cash(self): """ 资金中签率 """ if self.invest_cash_return_ratio: return self.invest_cash_return_ratio if self.status == "已上市" and self.invest_cash: try: return self.offer_cash / self.invest_cash except: pass @property def result_ratio_mv(self): """ 市值中签率 """ if self.status == "已上市" and self.invest_mv: return self.offer_mv * (1 - self.offer_cash_ratio) \ / self.invest_mv @property def offer_min_invest(self): """ 必中最低申购资金 """ if self.status == "已上市": if self.invest_cash or self.invest_cash_return_ratio: magnitude = math.ceil(math.log10(1 / self.result_ratio_cash)) return self.offer_price * 10 ** magnitude \ * (1 + self.offer_fee) @property def cashout_at(self): """ 出金日期 """ c = get_conf(self.exchange) return self.offset(c['cashout']) def offset(self, incr): c = get_conf(self.exchange) notrade = [int(x) for x in str(c['notrade'] or '').split(',') if x] delta = 1 if incr > 0 else -1 odate = self.offers_at while incr != 0: incr -= delta odate += timedelta(days=delta) while odate.weekday() in notrade: odate += timedelta(days=delta) return odate @property def total_offer_cash(self): return sum(c.offer_cash or 0 for c in Collection.cached(300) .query({'exchange': self.exchange, 'offers_at': self.offers_at})) @property def expected_invest_cash(self): ex = Exchange.find_exchange(self.exchange) if ex.expected_invest_cash and self.total_offer_cash: return (self.offer_cash or 0) / self.total_offer_cash \ * ex.expected_invest_cash @property def expected_invest_mv(self): ex = Exchange.find_exchange(self.exchange) if ex.total_market_value: return ex.total_market_value @property def expected_result_mv_ratio(self): """ 预期市值中签率 """ if not self.result_ratio_cash: if self.expected_invest_mv and self.offer_mv: return self.offer_mv / self.expected_invest_mv @property def expected_annual_profit(self): """ 预期年化收益率 """ ex = Exchange.find_exchange(self.exchange) if ex.expected_invest_cash and ex.median_increase: return self.expected_result_cash_ratio * ex.median_increase @property def expected_result_cash_ratio(self): """ 预期资金中签率 """ if not self.result_ratio_cash: ex = Exchange.find_exchange(self.exchange) if ex.expected_invest_cash: return self.total_offer_cash / ex.expected_invest_cash else: return self.result_ratio_cash
class ProfitLog(Document): """ 用户的收益日志 """ class Meta: idx1 = Index(['user', 'date'], unique=True) user = StringField(required=True) date = DateTimeField(required=True) profit = FloatField() @classmethod def profits(cls, user): """ 获得收益日志 """ return [{ 'date': pl.date, 'profit': pl.profit } for pl in cls.query({'user': user}, sort=[('date', 1)])] @classmethod def ensure_all_profits(cls): for u in User.query(): cls.ensure_profits(u._id) @classmethod def ensure_profits(cls, user): """ 确保生成收益日志 """ log.info('为用户{}生成收益日志'.format(user)) ts = list(reversed(Transaction.user_recent_transactions(user))) if ts: today = datetime.utcnow() + timedelta(hours=8) today = today.replace(hour=0, minute=0, second=0, microsecond=0) di = 0 date = ts[di].operated_at positions = {} realized_profits = defaultdict(float) while date <= today: profit = 0 # include new transactions while di < len(ts) and ts[di].operated_at <= date: t = ts[di] op = 1 if t.type_ == 'buy' else -1 pair = (t.exchange, t.symbol) if pair not in positions: positions[pair] = (t.price, t.quantity * op) else: pv = positions[pair] amount = pv[0] * pv[1] + t.price * t.quantity * op quantity = pv[1] + t.quantity * op if quantity == 0: realized_profits[pair] += pv[1] * (t.price - pv[0]) del positions[pair] else: positions[pair] = (amount / quantity, quantity) di += 1 # calculate profit for pair in positions: q = Quote.query_one( { 'exchange': pair[0], 'symbol': pair[1], 'quote_type': '1d', 'quote_at': { '$lte': date } }, sort=[('quote_at', -1)]) if q: pv = positions[pair] profit += (q.close - pv[0]) * pv[1] profit += sum(realized_profits.values()) # update profit cls.update_one({ 'date': date, 'user': user }, {'$set': { 'profit': profit }}, upsert=True) date += timedelta(days=1)