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 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 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 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 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 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 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 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 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 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