Ejemplo n.º 1
0
class Terminal(object):
    """
    用于和前端交互的数据终端,可用于交互的方法和属性有:
    - update_position(pos_string): 接收前端的持仓字符串,刷新position和snapshot,仅在交易日09:30-15:00有效
    - save(): 保存最新的仓位,仅在交易日15:00后有效
    - reload_snapshot: 刷新snapshot,返回刷新后的overall_info和detail_info
    - snapshot_overall_info: 最新的overall_info
    - snapshot_detail_info: 最新的detail_info
    - latest_position_string: 最新的position的简单字符串形式,以供修改
    """

    def __init__(self):
        self._benchmark = None
        self._benchmark_history = None
        self._benchmark_return = None
        self._portfolio_history = None
        self._portfolio_return = None
        self._trading_days = None

        self._positions = Positions()
        self._auto_fill_positions()
        self._load_benchmark()
        if not os.path.isfile(HISTORY_IMG_FILE):
            self._draw_history_timeline()

        self._snapshot = Snapshot(self._benchmark, self._positions.get_current_position())

    def _auto_fill_positions(self):
        now = datetime.now()
        today = now.strftime("%Y-%m-%d")
        yesterday = get_trading_days_relatively(today, -1)[0]

        trading_days_raw = self._positions._trading_days
        latest_valid_date = trading_days_raw[-1]
        latest_position = self._positions.get_position(latest_valid_date)

        trading_days_new = get_trading_days(latest_valid_date, yesterday)[1:]
        self._trading_days = trading_days_raw + trading_days_new
        if today in TRADING_DAYS_DICT:
            self._positions.set_current_info(today, deepcopy(latest_position))
        else:
            self._positions.set_current_info(yesterday, deepcopy(latest_position))

        if trading_days_new:
            logger.info("Filling positions data of {} through scraper, referring position at {}...".format(trading_days_new, latest_valid_date))

            for date in trading_days_new:
                temp_pos = deepcopy(latest_position)
                universe = temp_pos['securities'].keys()
                data = load_crossectional_close_prices(universe, date)

                value = temp_pos['cash']
                for sec, p in data.iteritems():
                    temp_pos['securities'][sec]['price'] = p
                    value += p * temp_pos['securities'][sec]['amount']
                temp_pos['value'] = value
                self._positions.set_info(date, temp_pos, is_new_date=True)
            logger.info("Successfully filled positions data!")

            self._positions.save_all()
            logger.info("Latest positions data saved at /lib/posdb/.")

        self._portfolio_history = self._positions.history_values
        v0 = self._portfolio_history[0]
        self._portfolio_return = [v/v0 - 1 for v in self._portfolio_history]

    def _load_benchmark(self):
        logger.info("Loading benchmark data file...")

        config = json.load(file(CONFIG_FILE))
        benchmark = config["benchmark"]
        start = self._trading_days[0]
        end = self._trading_days[-1]
        redraw = False

        try:
            benchmark_info = json.load(file(BENCHMARK_CACHE_FILE))
            assert benchmark_info['sec_id'] == benchmark
            assert benchmark_info['start'] == start
            assert benchmark_info['end'] == end
            logger.info("Successfully loaded benchmark data file!")
        except:
            redraw = True
            logger.info("Benchmark data file is outdated. Downloading benchmark data ({}) from {} to {} through scraper...".format(benchmark, start, end))

            data = load_daily_close_prices(universe=[benchmark],
                                           start=start, end=end)
            assert len(data[benchmark]) == len(self._trading_days)
            logger.info("Successfully downloaded benchmark data!")

            benchmark_info = {
                "sec_id": benchmark,
                "start": start,
                "end": end,
                "data": data[benchmark],
            }
            f = open(BENCHMARK_CACHE_FILE, 'w')
            f.write(json.dumps(benchmark_info, sort_keys=True, indent=4))
            f.close()
            logger.info("Benchmark data saved at ./static/temp/benchmark.json.")

        self._benchmark = benchmark
        self._benchmark_history = benchmark_info['data']
        assert len(self._benchmark_history) == len(self._portfolio_history)
        v0 = self._benchmark_history[0]
        self._benchmark_return = [v/v0 - 1 for v in self._benchmark_history]
        if redraw:
            self._draw_history_timeline()

    def _draw_history_timeline(self):
        fig = Figure(figsize=(7, 3))
        canvas = FigureCanvas(fig)
        ax_ret = fig.add_subplot(121, axisbg='white')
        ax_val = fig.add_subplot(122, axisbg='white')

        d = len(self._trading_days)
        xseries = range(d)
        xlabels = [t[-5:] for t in self._trading_days]
        pct = lambda x, _: '{0:1.1f}%'.format(100 * x)

        if d > 5:
            tseries = xseries[::d//5+1] + xseries[-1:]
            tlabels = xlabels[::d//5+1] + xlabels[-1:]

        # draw return lines
        ax_ret.clear()
        ax_ret.plot(xseries, self._portfolio_return, label='portfolio', linewidth=1.0, color='r')
        ax_ret.plot(xseries, self._benchmark_return, label='benchmark', linewidth=1.0, color='b')

        ax_ret.yaxis.set_major_formatter(FuncFormatter(pct))
        for item in ax_ret.get_yticklabels():
            item.set_size(9)
        ax_ret.set_xlim(0, d-1)
        ax_ret.set_xticks(tseries)
        ax_ret.set_xticklabels(tlabels, fontsize=9)
        ax_ret.set_title('Cumulative Return', fontsize=10)
        ax_ret.grid(True)
        ax_ret.legend(loc=2, prop={'size': 8})

        # draw value line
        ax_val.clear()
        ax_val.plot(xseries, self._portfolio_history, label='portfolio', linewidth=1.0, color='r')

        for item in ax_val.get_yticklabels():
            item.set_size(9)
        ax_val.set_xlim(0, d-1)
        ax_val.set_xticks(tseries)
        ax_val.set_xticklabels(tlabels, fontsize=9)
        ax_val.set_title('Portfolio Value', fontsize=10)
        ax_val.grid(True)

        fig.tight_layout()
        fig.savefig(HISTORY_IMG_FILE)
        logger.info("History image saved at ./static/temp/history.")

    def update_position(self, pos_string):
        new_position = self._parse_new_pos_string(pos_string)

        now = datetime.now()
        today = now.strftime("%Y-%m-%d")
        minute = now.strftime("%H:%M")
        if today in TRADING_DAYS_DICT and "09:30" <= minute <= "15:00":
            self._snapshot.update_position(new_position)
            self._positions.set_current_info(today, self._snapshot.latest_position())

    def _parse_new_pos_string(self, pos_string):
        pos_info = [r.strip() for r in pos_string.strip().split("\n")]
        pos_dict = {}
        pos_dict["cash"] = float(pos_info[0])
        pos_dict["securities"] = {}
        pos_dict["value"] = 0.0

        for r in pos_info[1:]:
            sec, amount = r.split("|")
            sec += ".XSHG" if sec[0] == "6" else ".XSHE"
            pos_dict["securities"][sec] = {
                "amount": float(amount),
                "price": 0.0,
                "name": None,
            }

        short_names = load_sec_shortname(pos_dict["securities"].keys())
        for sec, name in short_names.iteritems():
            pos_dict["securities"][sec]["name"] = name
        return pos_dict

    @property
    def save(self):
        now = datetime.now()
        today = now.strftime("%Y-%m-%d")
        minute = now.strftime("%H:%M")
        if today in TRADING_DAYS_DICT and minute > "15:00":
            self._snapshot.save()
            self._positions.set_current_info(today, self._snapshot.latest_position())
            self._positions.save_current_position()
        return ""

    @property
    def reload_snapshot(self):
        now = datetime.now()
        self._today = now.strftime("%Y-%m-%d")
        self._yesterday = get_trading_days_relatively(self._today, -1)
        self._minute = now.strftime("%H:%M")
        self._is_trading_day = self._today in TRADING_DAYS_DICT

        self._snapshot.refresh()
        return {
            "snapshot_overall_info": self.snapshot_overall_info,
            "snapshot_detail_info": self.snapshot_detail_info
        }

    @property
    def snapshot_overall_info(self):
        return self._snapshot.latest_overall_info_in_html()

    @property
    def snapshot_detail_info(self):
        return self._snapshot.latest_securities_details_in_html()

    @property
    def latest_position_string(self):
        return self._snapshot.latest_position_in_simple_string()