text: # -*- coding: utf-8 -*-
  import json
  import numbers
  import os
  import re
  import time
  
  import requests
  
  from . import exceptions, helpers, webtrader
  from .log import log
  
  
  class XueQiuTrader(webtrader.WebTrader):
      config_path = os.path.dirname(__file__) + "/config/xq.json"
  
      _HEADERS = {
          "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) "
                        "AppleWebKit/537.36 (KHTML, like Gecko) "
                        "Chrome/64.0.3282.167 Safari/537.36",
          "Host": "xueqiu.com",
          "Pragma": "no-cache",
          "Connection": "keep-alive",
          "Accept": "*/*",
          "Accept-Encoding": "gzip, deflate, br",
          "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
          "Cache-Control": "no-cache",
          "Referer": "https://xueqiu.com/P/ZH004612",
          "X-Requested-With": "XMLHttpRequest",
      }
  
      def __init__(self, **kwargs):
          super(XueQiuTrader, self).__init__()
  
          # 资金换算倍数
          self.multiple = (
              kwargs["initial_assets"] if "initial_assets" in kwargs else 1000000
          )
          if not isinstance(self.multiple, numbers.Number):
              raise TypeError("initial assets must be number(int, float)")
          if self.multiple < 1e3:
              raise ValueError("雪球初始资产不能小于1000元，当前预设值 {}".format(self.multiple))
  
          self.s = requests.Session()
          self.s.verify = False
          self.s.headers.update(self._HEADERS)
          self.account_config = None
  
      def autologin(self, **kwargs):
          """
          使用 cookies 之后不需要自动登陆
          :return:
          """
          self._set_cookies(self.account_config["cookies"])
  
      def _set_cookies(self, cookies):
          """设置雪球 cookies，代码来自于
          https://github.com/shidenggui/easytrader/issues/269
          :param cookies: 雪球 cookies
          :type cookies: str
          """
          cookie_dict = helpers.parse_cookies_str(cookies)
          self.s.cookies.update(cookie_dict)
  
      def _prepare_account(self, user="", password="", **kwargs):
          """
          转换参数到登录所需的字典格式
          :param cookies: 雪球登陆需要设置 cookies， 具体见
              https://smalltool.github.io/2016/08/02/cookie/
          :param portfolio_code: 组合代码
          :param portfolio_market: 交易市场， 可选['cn', 'us', 'hk'] 默认 'cn'
          :return:
          """
          if "portfolio_code" not in kwargs:
              raise TypeError("雪球登录需要设置 portfolio_code(组合代码) 参数")
          if "portfolio_market" not in kwargs:
              kwargs["portfolio_market"] = "cn"
          if "cookies" not in kwargs:
              raise TypeError(
                  "雪球登陆需要设置 cookies， 具体见"
                  "https://smalltool.github.io/2016/08/02/cookie/"
              )
          self.account_config = {
              "cookies": kwargs["cookies"],
              "portfolio_code": kwargs["portfolio_code"],
              "portfolio_market": kwargs["portfolio_market"],
          }
  
      def _virtual_to_balance(self, virtual):
          """
          虚拟净值转化为资金
          :param virtual: 雪球组合净值
          :return: 换算的资金
          """
          return virtual * self.multiple
  
      def _get_html(self, url, data=None):
          return self.s.get(url, params=data)
  
      def _search_stock_info(self, code):
          """
          通过雪球的接口获取股票详细信息
          :param code: 股票代码 000001
          :return: 查询到的股票 {u'stock_id': 1000279, u'code': u'SH600325',
              u'name': u'华发股份', u'ind_color': u'#d9633b', u'chg': -1.09,
              u'ind_id': 100014, u'percent': -9.31, u'current': 10.62,
              u'hasexist': None, u'flag': 1, u'ind_name': u'房地产', u'type': None,
              u'enName': None}
              ** flag : 未上市(0)、正常(1)、停牌(2)、涨跌停(3)、退市(4)
          """
          data = {
              "code": str(code),
              "size": "300",
              "key": "47bce5c74f",
              "market": self.account_config["portfolio_market"],
          }
          r = self.s.get(self.config["search_stock_url"], params=data)
          stocks = json.loads(r.text)
          stocks = stocks["stocks"]
          stock = None
          if len(stocks) > 0:
              stock = stocks[0]
          return stock
  
      def _get_portfolio_info(self, portfolio_code):
          """
          获取组合信息
          :return: 字典
          """
          url = self.config["portfolio_url"] + portfolio_code
          html = self._get_html(url).text
          match_info = re.search(r"(?<=SNB.cubeInfo = ).*(?=;\n)", html)
          if match_info is None:
              log.error("cant get portfolio info")
              raise Exception
              return
          try:
              portfolio_info = json.loads(match_info.group())
          except Exception as e:
              log.error("get portfolio info error: {}".format(e))
              raise Exception
              return
          return portfolio_info
  
      def _get_top_cubes_info(self, period, size):
          """
          获取排名靠前的实盘组合列表
          :return: 列表
          """
          top_cube_symbol_list = []
          page = 1
          while len(top_cube_symbol_list) < size:
              data = {
                  "tid": "PAMID",
                  "period": period,
                  "page": page
              }
              r = self._get_html(self.config["top_cube_list_url"], data)
              cubes = json.loads(r.text)
              for top_cube in cubes['result_data']['list']:
                  if len(top_cube_symbol_list) < size:
                      top_cube_symbol_list.append(top_cube['symbol'])
                  else:
                      break
              page = page + 1
          return top_cube_symbol_list
  
      def get_balance(self):
          """
          获取账户资金状况
          :return:
          """
          portfolio_code = self.account_config.get("portfolio_code", "ch")
          portfolio_info = self._get_portfolio_info(portfolio_code)
          asset_balance = self._virtual_to_balance(
              float(portfolio_info["net_value"])
          )  # 总资产
          position = portfolio_info["view_rebalancing"]  # 仓位结构
          cash = asset_balance * float(position["cash"]) / 100
          market = asset_balance - cash
          return [
              {
                  "asset_balance": asset_balance,
                  "current_balance": cash,
                  "enable_balance": cash,
                  "market_value": market,
                  "money_type": u"人民币",
                  "pre_interest": 0.25,
              }
          ]
  
      def get_balance_for_follow(self):
          """
          获取账户资金状况
          :return:
          """
          portfolio_code = self.account_config.get("portfolio_code", "ch")
          portfolio_info = self._get_portfolio_info(portfolio_code)
          position = portfolio_info["view_rebalancing"]  # 仓位结构
          return position
  
      def _get_position(self, symbol=None):
          """
          获取雪球持仓
          :return:
          """
          if symbol is None:
              portfolio_code = self.account_config["portfolio_code"]
          else:
              portfolio_code = symbol
          portfolio_info = self._get_portfolio_info(portfolio_code)
          if portfolio_info is None:
              return
          position = portfolio_info["view_rebalancing"]  # 仓位结构
          stocks = position["holdings"]  # 持仓股票
          return stocks
  
      @staticmethod
      def _time_strftime(time_stamp):
          try:
              local_time = time.localtime(time_stamp / 1000)
              return time.strftime("%Y-%m-%d %H:%M:%S", local_time)
          # pylint: disable=broad-except
          except Exception:
              return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
  
      def get_position(self):
          """
          获取持仓
          :return:
          """
          xq_positions = self._get_position(symbol)
          balance = self.get_balance()[0]
          position_list = []
          for pos in xq_positions:
              volume = pos["weight"] * balance["asset_balance"] / 100
              position_list.append(
                  {
                      "cost_price": volume / 100,
                      "current_amount": 100,
                      "enable_amount": 100,
                      "income_balance": 0,
                      "keep_cost_price": volume / 100,
                      "last_price": volume / 100,
                      "market_value": volume,
                      "position_str": "random",
                      "stock_code": pos["stock_symbol"],
                      "stock_name": pos["stock_name"],
                  }
              )
          return position_list
  
      def get_position_for_xq(self, symbol=None, cookies=None):
          """
          获取持仓
          :return:
          """
          if cookies is not None:
              self._set_cookies(cookies)
          xq_positions = self._get_position(symbol)
          position_list = []
          if xq_positions is not None:
              for pos in xq_positions:
                  position_list.append({
                      "stock_symbol": pos["stock_symbol"],
                      "stock_name": pos["stock_name"],
                      "weight": pos["weight"],
                      "segment_id": pos["segment_id"],
                      "segment_name": pos["segment_name"]
                  })
          return position_list
  
      def _get_xq_history(self):
          """
          获取雪球调仓历史
          :param instance:
          :param owner:
          :return:
          """
          data = {
              "cube_symbol": str(self.account_config["portfolio_code"]),
              "count": 20,
              "page": 1,
          }
          resp = self.s.get(self.config["history_url"], params=data)
          res = json.loads(resp.text)
          return res["list"]
  
      @property
      def history(self):
          return self._get_xq_history()
  
      def get_entrust(self):
          """
          获取委托单(目前返回20次调仓的结果)
          操作数量都按1手模拟换算的
          :return:
          """
          xq_entrust_list = self._get_xq_history()
          entrust_list = []
          replace_none = lambda s: s or 0
          for xq_entrusts in xq_entrust_list:
              status = xq_entrusts["status"]  # 调仓状态
              if status == "pending":
                  status = "已报"
              elif status in ["canceled", "failed"]:
                  status = "废单"
              else:
                  status = "已成"
              for entrust in xq_entrusts["rebalancing_histories"]:
                  price = entrust["price"]
                  entrust_list.append(
                      {
                          "entrust_no": entrust["id"],
                          "entrust_bs": u"买入"
                          if entrust["target_weight"]
                             > replace_none(entrust["prev_weight"])
                          else u"卖出",
                          "report_time": self._time_strftime(
                              entrust["updated_at"]
                          ),
                          "entrust_status": status,
                          "stock_code": entrust["stock_symbol"],
                          "stock_name": entrust["stock_name"],
                          "business_amount": 100,
                          "business_price": price,
                          "entrust_amount": 100,
                          "entrust_price": price,
                      }
                  )
          return entrust_list
  
      def cancel_entrust(self, entrust_no):
          """
          对未成交的调仓进行伪撤单
          :param entrust_no:
          :return:
          """
          xq_entrust_list = self._get_xq_history()
          is_have = False
          for xq_entrusts in xq_entrust_list:
              status = xq_entrusts["status"]  # 调仓状态
              for entrust in xq_entrusts["rebalancing_histories"]:
                  if entrust["id"] == entrust_no and status == "pending":
                      is_have = True
                      buy_or_sell = (
                          "buy"
                          if entrust["target_weight"] < entrust["weight"]
                          else "sell"
                      )
                      if (
                              entrust["target_weight"] == 0
                              and entrust["weight"] == 0
                      ):
                          raise exceptions.TradeError(u"移除的股票操作无法撤销,建议重新买入")
                      balance = self.get_balance()[0]
                      volume = (
                              abs(entrust["target_weight"] - entrust["weight"])
                              * balance["asset_balance"]
                              / 100
                      )
                      r = self._trade(
                          security=entrust["stock_symbol"],
                          volume=volume,
                          entrust_bs=buy_or_sell,
                      )
                      if len(r) > 0 and "error_info" in r[0]:
                          raise exceptions.TradeError(
                              u"撤销失败!%s" % ("error_info" in r[0])
                          )
          if not is_have:
              raise exceptions.TradeError(u"撤销对象已失效")
          return True
  
      def adjust_weight(self, stock_code, weight):
          """
          雪球组合调仓, weight 为调整后的仓位比例
          :param stock_code: str 股票代码
          :param weight: float 调整之后的持仓百分比， 0 - 100 之间的浮点数
          """
  
          stock = self._search_stock_info(stock_code)
          if stock is None:
              raise exceptions.TradeError(u"没有查询要操作的股票信息")
          if stock["flag"] != 1:
              raise exceptions.TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。")
  
          # 仓位比例向下取两位数
          weight = round(weight, 2)
          # 获取原有仓位信息
          position_list = self._get_position()
  
          # 调整后的持仓
          for position in position_list:
              if position["stock_id"] == stock["stock_id"]:
                  position["proactive"] = True
                  position["weight"] = weight
  
          if weight != 0 and stock["stock_id"] not in [
              k["stock_id"] for k in position_list
          ]:
              position_list.append(
                  {
                      "code": stock["code"],
                      "name": stock["name"],
                      "enName": stock["enName"],
                      "hasexist": stock["hasexist"],
                      "flag": stock["flag"],
                      "type": stock["type"],
                      "current": stock["current"],
                      "chg": stock["chg"],
                      "percent": str(stock["percent"]),
                      "stock_id": stock["stock_id"],
                      "ind_id": stock["ind_id"],
                      "ind_name": stock["ind_name"],
                      "ind_color": stock["ind_color"],
                      "textname": stock["name"],
                      "segment_name": stock["ind_name"],
                      "weight": weight,
                      "url": "/S/" + stock["code"],
                      "proactive": True,
                      "price": str(stock["current"]),
                  }
              )
  
          remain_weight = 100 - sum(i.get("weight") for i in position_list)
          cash = round(remain_weight, 2)
          log.debug("调仓比例:%f, 剩余持仓 :%f", weight, remain_weight)
          data = {
              "cash": cash,
              "holdings": str(json.dumps(position_list)),
              "cube_symbol": str(self.account_config["portfolio_code"]),
              "segment": "true",
              "comment": "",
          }
  
          try:
              resp = self.s.post(self.config["rebalance_url"], data=data)
          # pylint: disable=broad-except
          except Exception as e:
              log.warning("调仓失败: %s ", e)
              return None
          log.debug("调仓 %s: 持仓比例%d", stock["name"], weight)
          resp_json = json.loads(resp.text)
          if "error_description" in resp_json and resp.status_code != 200:
              log.error("调仓错误: %s", resp_json["error_description"])
              return [
                  {
                      "error_no": resp_json["error_code"],
                      "error_info": resp_json["error_description"],
                  }
              ]
          log.debug("调仓成功 %s: 持仓比例%d", stock["name"], weight)
          return None
  
      def _trade(self, security, price=0, amount=0, volume=0, entrust_bs="buy"):
          """
          调仓
          :param security:
          :param price:
          :param amount:
          :param volume:
          :param entrust_bs:
          :return:
          """
          stock = self._search_stock_info(security)
          balance = self.get_balance()[0]
          if stock is None:
              raise exceptions.TradeError(u"没有查询要操作的股票信息")
          if not volume:
              volume = int(float(price) * amount)  # 可能要取整数
          if balance["current_balance"] < volume and entrust_bs == "buy":
              raise exceptions.TradeError(u"没有足够的现金进行操作")
          if stock["flag"] != 1:
              raise exceptions.TradeError(u"未上市、停牌、涨跌停、退市的股票无法操作。")
          if volume == 0:
              raise exceptions.TradeError(u"操作金额不能为零")
  
          # 计算调仓调仓份额
          weight = volume / balance["asset_balance"] * 100
          weight = round(weight, 2)
  
          # 获取原有仓位信息
          position_list = self._get_position()
  
          # 调整后的持仓
          is_have = False
          for position in position_list:
              if position["stock_id"] == stock["stock_id"]:
                  is_have = True
                  position["proactive"] = True
                  old_weight = position["weight"]
                  if entrust_bs == "buy":
                      position["weight"] = weight + old_weight
                  else:
                      if weight > old_weight:
                          raise exceptions.TradeError(u"操作数量大于实际可卖出数量")
                      else:
                          position["weight"] = old_weight - weight
                  position["weight"] = round(position["weight"], 2)
          if not is_have:
              if entrust_bs == "buy":
                  position_list.append(
                      {
                          "code": stock["code"],
                          "name": stock["name"],
                          "enName": stock["enName"],
                          "hasexist": stock["hasexist"],
                          "flag": stock["flag"],
                          "type": stock["type"],
                          "current": stock["current"],
                          "chg": stock["chg"],
                          "percent": str(stock["percent"]),
                          "stock_id": stock["stock_id"],
                          "ind_id": stock["ind_id"],
                          "ind_name": stock["ind_name"],
                          "ind_color": stock["ind_color"],
                          "textname": stock["name"],
                          "segment_name": stock["ind_name"],
                          "weight": round(weight, 2),
                          "url": "/S/" + stock["code"],
                          "proactive": True,
                          "price": str(stock["current"]),
                      }
                  )
              else:
                  raise exceptions.TradeError(u"没有持有要卖出的股票")
  
          if entrust_bs == "buy":
              cash = (
                      (balance["current_balance"] - volume)
                      / balance["asset_balance"]
                      * 100
              )
          else:
              cash = (
                      (balance["current_balance"] + volume)
                      / balance["asset_balance"]
                      * 100
              )
          cash = round(cash, 2)
          log.debug("weight:%f, cash:%f", weight, cash)
  
          data = {
              "cash": cash,
              "holdings": str(json.dumps(position_list)),
              "cube_symbol": str(self.account_config["portfolio_code"]),
              "segment": 1,
              "comment": "",
          }
  
          try:
              resp = self.s.post(self.config["rebalance_url"], data=data)
          # pylint: disable=broad-except
          except Exception as e:
              log.warning("调仓失败: %s ", e)
              return None
          else:
              log.debug(
                  "调仓 %s%s: %d", entrust_bs, stock["name"], resp.status_code
              )
              resp_json = json.loads(resp.text)
              if "error_description" in resp_json and resp.status_code != 200:
                  log.error("调仓错误: %s", resp_json["error_description"])
                  return [
                      {
                          "error_no": resp_json["error_code"],
                          "error_info": resp_json["error_description"],
                      }
                  ]
              return [
                  {
                      "entrust_no": resp_json["id"],
                      "init_date": self._time_strftime(resp_json["created_at"]),
                      "batch_no": "委托批号",
                      "report_no": "申报号",
                      "seat_no": "席位编号",
                      "entrust_time": self._time_strftime(
                          resp_json["updated_at"]
                      ),
                      "entrust_price": price,
                      "entrust_amount": amount,
                      "stock_code": security,
                      "entrust_bs": "买入",
                      "entrust_type": "雪球虚拟委托",
                      "entrust_status": "-",
                  }
              ]
  
      def buy(self, security, price=0, amount=0, volume=0, entrust_prop=0):
          """买入卖出股票
          :param security: 股票代码
          :param price: 买入价格
          :param amount: 买入股数
          :param volume: 买入总金额 由 volume / price 取整， 若指定 price 则此参数无效
          :param entrust_prop:
          """
          return self._trade(security, price, amount, volume, "buy")
  
      def sell(self, security, price=0, amount=0, volume=0, entrust_prop=0):
          """卖出股票
          :param security: 股票代码
          :param price: 卖出价格
          :param amount: 卖出股数
          :param volume: 卖出总金额 由 volume / price 取整， 若指定 price 则此参数无效
          :param entrust_prop:
          """
          return self._trade(security, price, amount, volume, "sell")
  
      def get_top_cube_list(self, period, page):
          return self._get_top_cubes_info(period, page)
  