class BankAccount(EmbeddedDocument): """ 银行账号 """ bank = StringField(required=True) # 建设银行/... number = StringField(required=True) front = BinaryField() back = BinaryField()
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 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 Account(Document): """ 跌零用户的账号 """ class Meta: idf = IDFormatter('{user_id}_{login_name}') user_id = StringField(required=True) login_name = StringField(required=True) login_password = StringField(required=True)
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 Investor(Document): """ 投资人 """ class Meta: idf = IDFormatter('{id_number}') idx1 = Index(['user', 'order'], unique=True) user = StringField(required=True) # 用户 order = IntField(required=True) # 顺序 name = StringField(required=True) id_type = StringField(required=True) # 身份证/.. id_number = StringField(required=True) # 号码 id_front = BinaryField() # 照片 id_back = BinaryField() mobile = StringField(required=True) province = StringField(required=True) city = StringField(required=True) address = StringField(required=True) bank_accounts = ListField(EmbeddedField(BankAccount)) @classmethod def get_user_order(cls, user): i = cls.query_one({'user': user}, sort=[('order', -1)], limit=1) if not i: return 1 else: return i.order + 1 @classmethod def user_investors(cls, user): return list(cls.query({'user': user}, sort=[('order', 1)]))
class WechatEvent(Document): """ 微信事件 暂时只保存, 不处理 """ xml = StringField() # xml格式数据主题 updated_at = DateTimeField(modified=True)
class WechatAccessToken(Document): """ 微信ACCESS_TOKEN """ class Meta: idf = IDFormatter('{access_token}') idx1 = Index('expires_at', expireAfterSeconds=7200) access_token = StringField(required=True) expires_in = IntField() expires_at = DateTimeField(required=True) updated_at = DateTimeField(modified=True) @classmethod def get_access_token(cls): instance = cls.query_one() if instance: return instance['access_token'] else: appid = ybk.config.conf.get('wechat_appid') appsecret = ybk.config.conf.get('wechat_appsecret') grant_type = 'client_credential' url = 'https://api.weixin.qq.com/cgi-bin/token' params = { 'grant_type': grant_type, 'appid': appid, 'secret': appsecret, } j = requests.get(url, params=params, timeout=(3, 7)).json() expires_at = datetime.utcnow() + timedelta(seconds=j['expires_in']) cls({ 'access_token': j['access_token'], 'expires_in': j['expires_in'], 'expires_at': expires_at }).save() return j['access_token']
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 Position(Document): """ 当日持仓汇总 """ class Meta: idf = IDFormatter('{user_id}_{date}') idx1 = Index(['user_id', 'date'], unique=True) user_id = StringField(required=True) date = DateTimeField(required=True) position_list = ListField(EmbeddedField(MyPosition))
class Announcement(Document): """ 公告信息 """ class Meta: idf = IDFormatter('{url}') idx1 = Index('url', unique=True) idx2 = Index(['updated_at', 'type_']) idx3 = Index(['published_at', 'type_']) idx4 = Index(['exchange', 'type_']) exchange = StringField(required=True) # 交易所简称(ID) type_ = StringField(required=True) # 申购("offer")/中签("result") url = StringField(required=True) # 公告链接 title = StringField() # 公告标题 html = StringField() # 原始html published_at = DateTimeField() # 交易所发布时间 updated_at = DateTimeField(modified=True) parsed = BooleanField(default=False)
class Status(Document): """ 当日挂单汇总 """ class Meta: idf = IDFormatter('{user_id}_{date}') idx1 = Index(['user_id', 'date'], unique=True) user_id = StringField(required=True) date = DateTimeField(required=True) status_list = ListField(EmbeddedField(MyStatus))
class Code(Document): """ 短信验证码 """ class Meta: idx1 = Index(['mobile', 'send_at']) mobile = StringField(required=True) code = StringField(required=True) text = StringField(required=True) sent_at = DateTimeField(created=True) @classmethod def can_create(cls, mobile, type_): if not re.compile('^\d{11}$').match(mobile): return False, '手机号码格式不正确' if type_ == 'register': u = User.query_one({'mobile': mobile}) if u and u.is_active(): return False, '该手机已注册' c = cls.query_one({'mobile': mobile}, sort=[('sent_at', -1)]) if c and c.sent_at >= datetime.utcnow() - timedelta(seconds=89): return False, '发送验证码间隔太频繁' return True, '' @classmethod def create_code(cls, mobile): code = '{:06d}'.format(random.randint(0, 999999)) text = '【{company}】您的验证码是{code}。如非本人操作,请忽略本短信' text = text.format(company='邮币卡369', code=code) c = cls({'mobile': mobile, 'code': code, 'text': text}) c.save() return c @classmethod def verify(cls, mobile, code): c = cls.query_one({'mobile': mobile}, sort=[('sent_at', -1)]) if c: if c.sent_at < datetime.utcnow() - timedelta(seconds=60 * 15): return False, '验证码超时' return c.code == code, '验证码(不)匹配' return False, '验证码未发送'
class UserAgent(Document): """User-agent :param name: UA name :param version: UA version :param os: os system :param hardwaretype: hardware type :param popularity: frequency, such as Very common :param createat: create time(localtime) """ class Meta: idf = IDFormatter(next_id) idx1 = Index('name') name = StringField(required=True) version = StringField() os = StringField() hardwaretype = StringField() popularity = StringField() createat = DateTimeField(created=True)
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 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 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 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 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 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 TradeAccount(Document): """ 交易账号 """ class Meta: idf = IDFormatter('{exchange}_{login_name}') idx1 = Index(['investor', 'exchange'], unique=True) idx2 = Index(['exchange', 'login_name'], unique=True) idx3 = Index('user') user = StringField(required=True) # 所属用户 investor = StringField(required=True) # 投资人 bank = StringField(required=True) # 工商银行/... exchange = StringField(required=True) # 交易所简称 login_name = StringField(required=True) # 账号 login_password = StringField(required=False) # 密码 money_password = StringField(required=False) # 资金密码 verify_message = StringField(default='请先更新状态', required=False) # 验证失败的原因 verified = BooleanField(default=False) # 验证通过 grab_buy_on = BooleanField(default=False) # 抢单买 grab_sell_on = BooleanField(default=False) # 抢单卖 position = ListField(EmbeddedField(MyPosition), default=[]) money = EmbeddedField(MyMoney, default={}) orders = ListField(EmbeddedField(Order), default=[]) order_status = ListField(EmbeddedField(OrderStatus), default=[]) @classmethod def user_accounts(cls, user): return list(cls.query({'user': user})) @property def investor_name(self): investors = list(Investor.cached(5).query({'user': self.user})) for i in investors: if i._id == self.investor: return i.name else: return '未找到' @property def money_position(self): return sum(p.price * p.quantity for p in self.position)
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 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 Exchange(Document): """ 交易所 """ class Meta: idf = IDFormatter('{abbr}') idx1 = Index('abbr', unique=True) name = StringField(required=True) abbr = StringField(required=True) # 简称 url = StringField(required=True) updated_at = DateTimeField(modified=True) @classmethod def _all(cls): return list(Exchange.cached(300).query()) @classmethod def find_exchange(cls, exchange): for ex in cls._all(): if ex.abbr == exchange: return ex @cached_property_ttl(300) def num_collections(self): """ 交易品种数 """ return Collection.count({'exchange': self.abbr}) @cached_property_ttl(300) def offers_per_month(self): """ 月均申购次数 """ months = set() days = set() for c in Collection.cached(300).query({'exchange': self.abbr}): if c.offers_at: days.add(c.offers_at.strftime('%Y%m%d')) months.add(c.offers_at.strftime('%Y%m')) if len(days) > 0: return len(days) / len(months) @cached_property_ttl(300) def average_increase(self): """ 平均涨幅 """ result = 0 count = 0 for c in Collection.cached(300).query({'exchange': self.abbr}): lprice = Quote.latest_price(c.exchange, c.symbol) price = c.offer_price if lprice and price: result += lprice / price - 1 count += 1 if count > 0: return result / count @cached_property_ttl(300) def median_increase(self): """ 中位数涨幅(半个月-3个月新品) """ incs = [] for c in Collection.cached(300).query({'exchange': self.abbr}): if offers_between(c, 15, 90): lprice = Quote.latest_price(c.exchange, c.symbol) price = c.offer_price if lprice and price: incs.append(lprice / price - 1) if incs: return sorted(incs)[len(incs) // 2] @cached_property_ttl(300) def average_trading_days(self): """ 平均上市时间 """ days = [] for c in Collection.cached(300).query({'exchange': self.abbr}): quotes = Quote.cached(300).query({'exchange': c.exchange, 'symbol': c.symbol, 'quote_type': '1d'}) days.append(len(quotes)) if len(days) > 0: return sum(days) / len(days) def _get_colls_by_day(self): colls = Collection.cached(300).query({'exchange': self.abbr}) colls = [c for c in colls if c.offers_at] colls = sorted(colls, key=lambda x: x.offers_at, reverse=False) return groupby(colls, lambda x: x.offers_at) @cached_property_ttl(300) def invest_cash_history(self): """ 申购资金历史记录 """ history = [] for offers_at, colls in self._get_colls_by_day(): colls = list(colls) invest_cash = sum(c.invest_cash_real or 0 for c in colls) if invest_cash: history.append({ 'date': colls[0].offers_at, 'invest_cash': invest_cash, 'total_cash': sum(c.invest_cash_real or 0 for c in Collection.cached(300) .query({'offers_at': colls[0].offers_at})) }) return history @cached_property_ttl(300) def offer_cash_history(self): """ 申购资金市值历史记录 """ history = [] for offers_at, colls in self._get_colls_by_day(): colls = list(colls) offer_cash = sum(c.offer_cash or 0 for c in colls) if offer_cash: history.append({ 'date': colls[0].offers_at, 'offer_cash': offer_cash, }) return history @cached_property_ttl(300) def result_ratio_history(self): """ 中签率历史记录 """ history = [] for offers_at, colls in self._get_colls_by_day(): colls = list(colls) total_cash = sum(c.offer_cash or 0 for c in colls) total_invest = sum(c.invest_cash_real or 0 for c in colls) if total_invest: result_ratio = total_cash / total_invest history.append({ 'date': colls[0].offers_at, 'result_ratio': result_ratio, }) return history @cached_property_ttl(300) def increase_history(self): """ 涨幅的历史记录 """ colls = Collection.cached(300).query({'exchange': self.abbr}) symbols = [c.symbol for c in colls if offers_between(c, 15, 90)] hdict = defaultdict(list) for q in Quote.cached(300).query({'exchange': self.abbr, 'symbol': {'$in': symbols}, 'quote_type': '1d'}, sort=[('quote_at', 1)]): if q.close: hdict[q.symbol].append(q.close) for symbol, values in hdict.items(): for i in range(1, len(values)): values[i] = values[i] / values[0] * 1.3 - 1 values[0] = 0.3 return hdict @cached_property_ttl(300) def average_result_cash_ratio(self): """ 近3次平均中签率 """ total_invest = 0 total_offer = 0 for hi, ho in zip(self.invest_cash_history[-3:], self.offer_cash_history[-3:]): total_invest += hi['invest_cash'] or 0 total_offer += ho['offer_cash'] or 0 if total_invest > 0 and total_offer > 0: return total_offer / total_invest @cached_property_ttl(300) def average_invest_cash(self): """ 平均申购资金 """ total_invest = 0 count = 0 for h in self.invest_cash_history[-3:]: total_invest += h['invest_cash'] or 0 count += 1 if count > 0 and total_invest > 0: return total_invest / 1e8 / count @cached_property_ttl(300) def expected_invest_cash(self): if not self.invest_cash_history: return None exp = 0 for i, h in enumerate(reversed(self.invest_cash_history[-3:])): exp += 0.7 * (0.3 ** i) * h['invest_cash'] exp += 0.3 * (0.3 ** i) * h['invest_cash'] return exp @cached_property_ttl(300) def rating(self): """ 评分 """ if self.average_result_cash_ratio and self.median_increase: return int(self.average_result_cash_ratio * self.median_increase / 0.003) @cached_property_ttl(300) def total_market_value(self): mv = 0 for c in Collection.query({'exchange': self.abbr}): lprice = Quote.latest_price(c.exchange, c.symbol) if c.offer_quantity and lprice: mv += c.offer_quantity * lprice * 10 return mv
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 User(Document): """ 用户 """ class Meta: idf = IDFormatter('{mobile}') idx1 = Index('mobile', unique=True) idx2 = Index('_is_admin') idx3 = Index('_is_active') idx4 = Index('username', unique=True) mobile = StringField(required=True) username = StringField() password = StringField() # bcrypt hashed invited_by = StringField() # 邀请人id ymoney = IntField(default=1000) # Y币 reserved_ymoney = IntField(default=0) # 预扣Y币 created_at = DateTimeField(created=True) last_login_at = DateTimeField(modified=True) auto_accounting = BooleanField(default=False) _is_active = BooleanField(default=False) # 通过验证 _is_admin = BooleanField(default=False) def add_to_admin(self): self._is_admin = True self.upsert() return True def activate(self): self._is_active = True self.upsert() def is_admin(self): return self._is_admin def is_active(self): return self._is_active def is_authenticated(self): return True def is_anonymous(self): return False def get_id(self): return self.mobile @classmethod def check_available(cls, mobile=None, username=None): u1 = cls.query_one({'mobile': mobile}) u2 = cls.query_one({'username': username}) if u1 or u2: return False else: return True @classmethod def create_user(cls, mobile, username, password, invited_by): if not cls.find_one({'_id': invited_by}): raise ValueError('请输入正确的邀请人手机号码') if not cls.check_available(mobile, username): raise ValueError('手机号码或用户名已被使用') u = cls({ 'mobile': mobile, 'username': username, 'password': cls.create_password(password), 'invited_by': invited_by, }) u.save() return u @classmethod def create_password(cls, password): return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') def change_password(self, password): self.password = self.create_password(password) self.upsert() @classmethod def check_login(cls, mobile, password): u = cls.query_one({'mobile': mobile}) password = password.encode('utf-8') if u: hashed = u.password.encode('utf-8') if bcrypt.hashpw(password, hashed) == hashed: return u return None
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)