Example #1
0
class ReturnsAnalyzerBase(stratanalyzer.StrategyAnalyzer):
    def __init__(self):
        super(ReturnsAnalyzerBase, self).__init__()
        self.__event = Event()
        self.__portfolioReturns = None

    @classmethod
    def getOrCreateShared(cls, strat):
        name = cls.__name__
        # Get or create the shared ReturnsAnalyzerBase.
        ret = strat.getNamedAnalyzer(name)
        if ret is None:
            ret = ReturnsAnalyzerBase()
            strat.attachAnalyzerEx(ret, name)
        return ret

    def attached(self):
        self.__portfolioReturns = TimeWeightedReturns(self.strat.broker.equity)

    # An event will be notified when return are calculated at each bar. The hander should receive 1 parameter:
    # 1: The current datetime.
    # 2: This analyzer's instance
    def getEvent(self):
        return self.__event

    def getNetReturn(self):
        return self.__portfolioReturns.getLastPeriodReturns()

    def getCumulativeReturn(self):
        return self.__portfolioReturns.getCumulativeReturns()

    def beforeOnBars(self, bars):
        self.__portfolioReturns.update(self.strat.broker.equity)

        # Notify that new returns are available.
        self.__event.emit(bars.datetime, self)
Example #2
0
class BarDataSeries:
    """
        需要获取某个价格序列的时候,直接调用对应的属性即可

        请注意:不要对其进行修改,不然会造成意想不到的后果

        BarDataSeries 会自动对每个分量进行相应的延展。因此你不用考虑可能存在的问题。拿来用就行
    """

    def __init__(self, bars=None, timestamps=None, current=0, max_len=None, frequency=Frequency.MINUTE):
        self.impl = BarDataSeriesImpl(
            bars=bars, timestamps=timestamps, current=current, max_len=max_len)
        self.frequency = frequency

        self.append_event = Event()

        # 这里我们已经将 implement中的相关数据传达了
        # 所以将相应信息交给reset处理即可

        self.ask_open = PriceSeries(self.append_event,
                                    self.impl.bars[:, 0], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.ask_close = PriceSeries(self.append_event,
                                     self.impl.bars[:, 1], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.ask_high = PriceSeries(self.append_event,
                                    self.impl.bars[:, 2], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.ask_low = PriceSeries(self.append_event,
                                   self.impl.bars[:, 3], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.bid_open = PriceSeries(self.append_event,
                                    self.impl.bars[:, 4], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.bid_close = PriceSeries(self.append_event,
                                     self.impl.bars[:, 5], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.bid_high = PriceSeries(self.append_event,
                                    self.impl.bars[:, 6], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.bid_low = PriceSeries(self.append_event,
                                   self.impl.bars[:, 7], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.volume = PriceSeries(self.append_event,
                                  self.impl.bars[:, 8], self.impl.timestamps, self.impl.current, self.impl.max_len)

    def __getitem__(self, key):
        return self.impl[key]

    @property
    def timestamps(self):
        # 截断后的时间戳
        return self.impl.current_timestamps()

    @property
    def bars(self):
        # 截断后的柱状数据
        return self.impl.current_bars()

    def reset(self, max_len=None):
        self.impl.reset(max_len)
        self.__reset_bar_series()

    def __reset_bar_series(self):
        self.ask_open.reset(
            self.impl.bars[:, 0], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.ask_close.reset(
            self.impl.bars[:, 1], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.ask_high.reset(
            self.impl.bars[:, 2], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.ask_low.reset(
            self.impl.bars[:, 3], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.bid_open.reset(
            self.impl.bars[:, 4], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.bid_close.reset(
            self.impl.bars[:, 5], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.bid_high.reset(
            self.impl.bars[:, 6], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.bid_low.reset(
            self.impl.bars[:, 7], self.impl.timestamps, self.impl.current, self.impl.max_len)
        self.volume.reset(
            self.impl.bars[:, 8], self.impl.timestamps, self.impl.current, self.impl.max_len)

    def append(self, bar: Bar):
        """
            利用Bar 内部的序列节约下来序列化的过程
        """

        # 1. 进行追加操作,并判断是否扩充

        extended = self.impl.append_with_datetime(bar.data, bar.start_date)

        # 2. 根据是否有扩充,来判断子列是否扩充

        # 说明:不将此任务放进队列是为了提高效率

        if extended:
            """
                注意,这里当Implement满的时候,BarSeries也重置一下即可。
            """
            self.__reset_bar_series()

        # 3. 再通知我们追加写入了
        self.append_event.emit(extended)

    def as_new(self):
        self.impl.current = 0

    def to_next(self):
        assert self.impl.current != self.impl.max_len

        self.impl.current += 1
        self.append_event.emit(False)

        return self.impl.current < self.impl.max_len
Example #3
0
class BaseBarFeed(basefeed.BaseFeed):
    def __init__(self, frequency, instruments, bars=None, maxLen=None):

        self.__bars = bars if bars is not None else []
        self.__bar_len = len(self.__bars)

        self.__bar_events = Event()
        self.__feed_reset_event = Event()

        self.__current_bar_index = 0

        self.__started = False
        self.__frequency = frequency

        self.__useAdjustedValues = None
        self.__defaultInstrument = None

        super(BaseBarFeed, self).__init__(maxLen)

        self.__instruments = instruments

        for instrument in instruments:
            self.register_instrument(instrument)

        try:
            self.__barsHaveAdjClose = self.__bars[0][instruments[0]].getAdjClose() is not None
        except Exception:
            self.__barsHaveAdjClose = False

    def reset(self):
        self.__started = False
        self.__current_bar_index = 0
        self.__feed_reset_event.emit(self.__bars)

    def clone(self):
        new_feed = BaseBarFeed(bars=self.bars, maxLen=self.max_len, frequency=self.__frequency,
                               instruments=self.__instruments)
        return new_feed

    @property
    def bar_events(self):
        return self.__bar_events

    @property
    def pos(self):
        return self.__current_bar_index

    @property
    def bars(self):
        return self.__bars

    @property
    def current_bars(self):
        """

        :return:
        """
        if self.__current_bar_index >= self.__bar_len:
            return None

        return self.bars[self.__current_bar_index]

    @property
    def next_bars(self):
        self.__current_bar_index += 1
        now = self.current_bars
        return now

    @property
    def current_datetime(self):
        return self.current_bars.datetime if self.current_bars else None

    @property
    def last_bars(self):
        return self.__bars[self.pos - 1] if self.pos > 0 and self.__current_bar_index < self.__bar_len + 1 else None

    def last_bar(self, instrument):
        return self.last_bars.bar(instrument) if self.last_bars is not None else None

    def start(self):
        super(BaseBarFeed, self).start()
        self.__started = True

    def stop(self):
        pass

    def join(self):
        pass

    def eof(self):
        return self.__current_bar_index + 1 >= self.__bar_len

    def peek_datetime(self):
        return self.current_datetime

    def dispatch(self):
        current_datetime, current_bars = self.getNextValuesAndUpdateDS()
        if current_bars is not None:
            self.__bar_events.emit(current_datetime, self.last_bars, self.current_bars)
        return current_bars is not None

    @bars.setter
    def bars(self, value):
        self.__bars = [Bars(bar_dict=i) for i in value]
        self.__bar_len = len(self.__bars)
        self.__current_bar_index = 0
        self.__started = False
        self.__feed_reset_event.emit(self.__bars)

    @property
    def instruments(self):
        """
            暂时我们只认第一个
        :return:
        """
        return self.__instruments

    @property
    def feed_reset_event(self):
        return self.__feed_reset_event


    @property
    def registered_instruments(self):
        """Returns a list of registered intstrument names."""
        return self.getKeys()

    def register_instrument(self, instrument):
        self.__defaultInstrument = instrument

    @property
    def default_instrument(self):
        return self.__defaultInstrument

    def dispatch_priority(self):
        return dispatchprio.BAR_FEED
    @property
    def next_values(self):
        values = self.next_bars
        return self.current_datetime, values

    @property
    def bars_have_adj_close(self):
        return self.__barsHaveAdjClose

    @property
    def frequency(self):
        return self.__frequency
Example #4
0
class BackTestBroker(BaseBroker):
    LOGGER_NAME = "back_test_log"

    def __init__(self,
                 cash: float,
                 bar_feed: BaseBarFeed,
                 commission: Commission,
                 round_quantity=lambda x: int(x)):

        self.__commission = commission
        self.__bar_feed = bar_feed
        self.__cash = self.__initial_cash = cash

        self.__next_order_id = 0
        self.__active_orders = {}
        self.__quantities = {}
        self.__logger = myalgo.logger.get_logger("Broker_log")

        self.__order_events = Event()

        self.__round = round_quantity

        self.__bar_feed.bar_events.subscribe(self.on_bars)
        self.__bar_feed.feed_reset_event.subscribe(self.__on_feed_change)
        self.__started = False

        super(BaseBroker, self).__init__(bar_feed.instruments)

    @property
    def quantities(self):
        return self.__quantities

    def __on_feed_change(self, bars):
        self.__reset()

    @property
    def started(self):
        return self.__started

    @property
    def next_order_id(self):
        ret = self.__next_order_id
        self.__next_order_id += 1
        return ret

    @property
    def order_events(self):
        return self.__order_events

    def register_order(self, order_: Order):
        assert (order_.id not in self.__active_orders)
        assert (order_.id is not None)
        self.__active_orders[order_.id] = order_

    def unregister_order(self, order_: Order):
        assert (order_.id in self.__active_orders)
        assert (order_.id is not None)
        del self.__active_orders[order_.id]

    @property
    def logger(self):
        return self.__logger

    def get_bar(self, bars, instrument):
        ret = bars.bar(instrument)
        if ret is None:
            ret = self.__bar_feed.last_bar(instrument)
        return ret

    def cash(self, include_short=True):
        ret = self.__cash
        if not include_short and self.__bar_feed.current_bars is not None:
            bars = self.__bar_feed.current_bars
            for instrument, shares in six.iteritems(self.__quantities):
                if shares < 0:
                    instrument_price = self.get_bar(bars, instrument).in_price
                    ret += instrument_price * shares
        return ret

    @property
    def equity(self):
        """Returns the portfolio value (cash + shares * price)."""
        ret = self.__cash
        for instrument, shares in six.iteritems(self.__quantities):
            if self.__bar_feed.last_bar(instrument) is None:
                continue
            instrument_price = self.__bar_feed.last_bar(instrument).out_price
            assert instrument_price is not None, "Price for %s is missing" % instrument
            ret += instrument_price * shares
        return ret

    def create_limit_order(self, action: Action, instrument: str,
                           limit_price: float, quantity: float):
        return LimitOrder(action,
                          instrument,
                          limit_price=limit_price,
                          quantity=quantity,
                          round_quantity=self.__round)

    def create_market_order(self,
                            action: Action,
                            instrument: str,
                            quantity: float,
                            on_close=False):
        # In order to properly support market-on-close with intraday feeds I'd need to know about different
        # exchange/market trading hours and support specifying routing an order to a specific exchange/market.
        # Even if I had all this in place it would be a problem while paper-trading with a live feed since
        # I can't tell if the next bar will be the last bar of the market session or not.
        return MarketOrder(action,
                           instrument,
                           quantity,
                           on_close,
                           round_quantity=self.__round)

    def create_stop_order(self, action: Action, instrument: str, price: float,
                          quantity: float):
        return StopOrder(action,
                         instrument,
                         price,
                         quantity,
                         round_quantity=self.__round)

    def create_stop_limit_order(self, action: Action, instrument: str,
                                stop_price: float, limit_price: float,
                                quantity: float):
        return StopLimitOrder(action,
                              instrument,
                              stop_price,
                              limit_price,
                              quantity,
                              round_quantity=self.__round)

    # Tries to commit an order execution.
    def commit_order_execution(self, fill_info: FillInfo, order_: Order,
                               datetime_: datetime):
        price = fill_info.price
        quantity = fill_info.quantity

        if order_.is_buy:
            cost = price * quantity * -1
            assert (cost < 0)
            shares_delta = quantity
        elif order_.is_sell:
            cost = price * quantity
            assert (cost > 0)
            shares_delta = quantity * -1
        else:  # Unknown action
            assert False

        commission = self.__commission.calculate(order_, price, quantity)
        cost -= commission
        resulting_cash = self.__cash + cost

        # Check that we're ok on cash after the commission.
        if resulting_cash >= 0:

            # Update the order before updating internal state since addExecutionInfo may raise.
            # addExecutionInfo should switch the order state.
            execution = Execution(price, quantity, commission, datetime_)
            order_.execute(execution)

            # Commit the order execution.
            self.__cash = resulting_cash

            updated_shares = order_.round_quantity(
                self.__quantities[order_.instrument] + shares_delta)
            self.__quantities[order_.instrument] = updated_shares

            # Notify the order update
            if order_.is_filled:
                self.unregister_order(order_)
                self.notify_order_event(
                    OrderEvent(order_, State.FILLED, execution))
            elif order_.is_partially_filled:
                self.notify_order_event(
                    OrderEvent(order_, State.PARTIALLY_FILLED, execution))
            else:
                assert False
        else:
            self.__logger.debug(
                "Not enough cash to fill %s order [%s] for %s share/s" %
                (order_.instrument, order_.id, order_.remain))

    def submit_order(self, order_: Order):
        if order_.is_initial:
            order_.submitted(self.next_order_id, self.current_datetime)
            self.register_order(order_)
            self.notify_order_event(OrderEvent(order_, State.SUBMITTED, None))
        else:
            raise Exception("The order was already processed")

    # Return True if further processing is needed.
    def __preprocess_order(self, order_: Order, bar_: Bar):
        ret = True

        # For non-GTC orders we need to check if the order has expired.
        if not order_.good_till_canceled:

            current = bar_.start_date

            expired = current.date() > order_.accepted_at.date()

            # Cancel the order if it is expired.
            if expired:
                ret = False
                self.unregister_order(order_)
                order_.canceled(current)
                self.notify_order_event(
                    OrderEvent(order_, State.CANCELED, "Expired"))

        return ret

    def __postprocess_order(self, order_: Order, bar_: Bar):
        # For non-GTC orders and daily (or greater) bars we need to check if orders should expire right now
        # before waiting for the next bar.
        if not order_.good_till_canceled:
            expired = False

            # Cancel the order if it will expire in the next bar.
            if expired:
                self.unregister_order(order_)
                order_.canceled(bar_.start_date)
                self.notify_order_event(
                    OrderEvent(order_, State.CANCELLED, "Expired"))

    def __process_order(self, order_, bar1: Bar, bar2: Bar):
        if not self.__preprocess_order(order_, bar2):
            return
        # Double dispatch to the fill strategy using the concrete order type.
        fill_info = order_.process(self, bar1, bar2)
        if fill_info is not None:
            self.commit_order_execution(fill_info, order_, bar2.start_date)

        if order_.is_active:
            self.__postprocess_order(order_, bar2)

    def __on_bars_impl(self, order_, bars1: Bars, bars2: Bars):
        # IF WE'RE DEALING WITH MULTIPLE INSTRUMENTS WE SKIP ORDER PROCESSING IF THERE IS NO BAR FOR THE ORDER'S
        # INSTRUMENT TO GET THE SAME BEHAVIOUR AS IF WERE BE PROCESSING ONLY ONE INSTRUMENT.
        bar1 = bars1.bar(order_.instrument)
        bar2 = bars2.bar(order_.instrument)
        if bar1 is not None and bar2 is not None:
            # Switch from SUBMITTED -> ACCEPTED
            if order_.is_submitted:
                order_.accepted(bar2.start_date)
                self.notify_order_event(
                    OrderEvent(order_, State.ACCEPTED, None))

            if order_.is_active:
                # This may trigger orders to be added/removed from __activeOrders.
                self.__process_order(order_, bar1, bar2)
            else:
                # If an order is not active it should be because it was c11anceled in this same loop and it should
                # have been removed.
                assert order_.is_canceled
                assert order_ not in self.__active_orders

    def on_bars(self, datetime_, bars1: Bars, bars2: Bars):

        # This is to froze the orders that will be processed in this event, to avoid new getting orders introduced
        # and processed on this very same event.
        orders_to_process = list(self.__active_orders.values())

        for order_ in orders_to_process:
            # This may trigger orders to be added/removed from __activeOrders.
            self.__on_bars_impl(order_, bars1, bars2)

    @property
    def active_instruments(self):
        return [
            instrument
            for instrument, shares in six.iteritems(self.__quantities)
            if shares != 0
        ]

    def active_orders(self, instrument=None):
        if instrument is None:
            ret = list(self.__active_orders.values())
        else:
            ret = [
                order_ for order_ in self.__active_orders.values()
                if order_.instrument == instrument
            ]
        return ret

    def cancel_order(self, order_: Order):
        active_order = self.__active_orders.get(order_.id)
        if active_order is None:
            raise Exception("The order is not active anymore")
        if active_order.is_filled:
            raise Exception("Can't cancel order that has already been filled")

        self.unregister_order(active_order)
        active_order.canceled(self.current_datetime)
        self.notify_order_event(
            OrderEvent(active_order, State.CANCELED,
                       "User requested cancellation"))

    @property
    def commission(self):
        """Returns the strategy used to calculate order commissions.

        :rtype: :class:`Commission`.
        """
        return self.__commission

    @commission.setter
    def commission(self, commission):
        """Sets the strategy to use to calculate order commissions.

        :param commission: An object responsible for calculating order commissions.
        :type commission: :class:`Commission`.
        """

        self.__commission = commission

    @property
    def current_datetime(self):
        return self.__bar_feed.current_datetime

    @property
    def bar_feed(self):
        return self.__bar_feed

    def start(self):
        super(BackTestBroker, self).start()
        self.__started = True

    def eof(self):
        # If there are no more events in the barfeed, then there is nothing left for us to do since all processing took
        # place while processing barfeed events.
        return self.bar_feed.eof()

    def peek_datetime(self):
        return None

    def notify_order_event(self, event: OrderEvent):
        return self.__order_events.emit(self, event)

    def __reset(self):
        self.__started = False
        self.__cash = self.__initial_cash
        self.__next_order_id = 0
        self.__active_orders = {}
        self.instruments = self.bar_feed.instruments
Example #5
0
class BaseStrategy:
    LOGGER_NAME = "BaseStrategyLog"

    def __init__(self, broker: BaseBroker, series_max_len=None):
        self.__broker = broker
        self.__activePositions = set()
        self.__orderToPosition = {}
        self.__barsProcessedEvent = Event()
        self.__analyzers = []
        self.__namedAnalyzers = {}
        self.__resampledBarFeeds = []
        self.__dispatcher = Dispatcher()
        self.__broker.order_events.subscribe(self.__onOrderEvent)
        self.bar_feed.bar_events.subscribe(self.__onBars)
        self.bar_feed.feed_reset_event.subscribe(self.reset)

        # onStart will be called once all subjects are started.
        self.__dispatcher.getStartEvent().subscribe(self.onStart)
        self.__dispatcher.getIdleEvent().subscribe(self.__onIdle)

        # It is important to dispatch broker events before feed events, specially if we're backtesting.
        self.__dispatcher.addSubject(self.broker)
        self.__dispatcher.addSubject(self.bar_feed)

        # Initialize logging.
        self.__logger = logger.get_logger(BaseStrategy.LOGGER_NAME)

        self.use_event_datetime_logs = True

        self.__onEnterOkEvent = Event()
        self.__onEnterCanceledEvent = Event()
        self.__onExitOkEvent = Event()
        self.__onExitCanceledEvent = Event()

        self.__onEnterOkEvent.subscribe(self.onEnterOk)
        self.__onEnterCanceledEvent.subscribe(self.onEnterCanceled)
        self.__onExitOkEvent.subscribe(self.onExitOk)
        self.__onExitCanceledEvent.subscribe(self.onExitCanceled)
        self.__onEnterStartEvent = Event()
        self.__onExitStartEvent = Event()
        """
            我们尝试把数据序列记在策略中
            TODO: 对于离线的回测,这个过程可以利用本来的数据
        """

        instruments = broker.instruments

        self.bar_series = dict()
        self.max_series_length = series_max_len

        for instrument in instruments:
            self.bar_series[instrument] = BarDataSeries(max_len=series_max_len)
        """
            反正都要用,为何不把所有Positions管理起来呢
        """
        self.__positions = []
        self.__onEnterStartEvent.subscribe(self.__on_enter_start)

    def __on_enter_start(self, position):
        self.__positions.append(position)

    @property
    def positions(self):
        return self.__positions

    @property
    def enter_start_event(self):
        return self.__onEnterStartEvent

    @property
    def exit_start_event(self):
        return self.__onExitStartEvent

    @property
    def enter_ok_event(self):
        return self.__onEnterOkEvent

    @property
    def exit_ok_event(self):
        return self.__onExitOkEvent

    @property
    def enter_canceled_event(self):
        return self.__onEnterCanceledEvent

    @property
    def exit_canceled_event(self):
        return self.__onExitCanceledEvent

    def notify_enter_start(self, position):
        self.__onEnterStartEvent.emit(position)

    def notify_exit_start(self, position):
        self.__onExitStartEvent.emit(position)

    def notify_enter_ok(self, position):
        self.__onEnterOkEvent.emit(position)

    def notify_enter_canceled(self, position):
        self.__onEnterCanceledEvent.emit(position)

    def notify_exit_canceled(self, position):
        self.__onExitCanceledEvent.emit(position)

    def notify_exit_ok(self, position):
        self.__onExitOkEvent.emit(position)

    def reset(self, bars):
        self.__activePositions = set()
        self.__orderToPosition = {}
        self.__barsProcessedEvent = Event()
        self.__analyzers = []
        self.__namedAnalyzers = {}
        self.__resampledBarFeeds = []
        self.__dispatcher = Dispatcher()
        for series in self.bar_series.values():
            series.reset(self.max_series_length)

    @property
    def use_event_datetime_logs(self):
        return logger.Formatter.DATETIME_HOOK is self.dispatcher.getCurrentDateTime

    @use_event_datetime_logs.setter
    def use_event_datetime_logs(self, use: bool):
        if use:
            logger.Formatter.DATETIME_HOOK = self.dispatcher.getCurrentDateTime
        else:
            logger.Formatter.DATETIME_HOOK = None

    @property
    def broker(self):
        return self.__broker

    @property
    def bars_processed_events(self):
        return self.__barsProcessedEvent

    @property
    def bar_feed(self):
        return self.broker.bar_feed

    @property
    def feed(self):
        return self.bar_feed

    @property
    def current_datetime(self):
        return self.bar_feed.current_datetime

    @property
    def dispatcher(self):
        return self.__dispatcher

    def last_price(self, instrument: str):
        ret = None
        bar = self.bar_feed.last_bar(instrument)
        if bar is not None:
            ret = bar.price
        return ret

    def limitOrder(self,
                   instrument: str,
                   limit_price: float,
                   quantity: float,
                   goodTill_canceled: bool = False,
                   all_or_none: bool = False):
        ret = None
        if quantity > 0:
            ret: LimitOrder = self.broker.create_limit_order(
                Action.BUY, instrument, limit_price, quantity)
        elif quantity < 0:
            ret: LimitOrder = self.broker.create_limit_order(
                Action.SELL, instrument, limit_price, quantity * -1)
        if ret:
            ret.good_till_canceled = goodTill_canceled
            ret.all_or_one = all_or_none
            self.broker.submit_order(ret)
        return ret

    def stopLimitOrder(self,
                       instrument,
                       stopPrice,
                       limitPrice,
                       quantity,
                       goodTillCanceled=False,
                       allOrNone=False):
        """Submits a stop limit order.

        :param instrument: Instrument identifier.
        :type instrument: string.
        :param stopPrice: Stop price.
        :type stopPrice: float.
        :param limitPrice: Limit price.
        :type limitPrice: float.
        :param quantity: The amount of shares. Positive means buy, negative means sell.
        :type quantity: int/float.
        :param goodTillCanceled: True if the order is good till canceled. If False then the order gets automatically canceled when the session closes.
        :type goodTillCanceled: boolean.
        :param allOrNone: True if the order should be completely filled or not at all.
        :type allOrNone: boolean.
        :rtype: The :class:`pyalgotrade.broker.StopLimitOrder` submitted.
        """

        ret = None
        if quantity > 0:
            ret = self.broker.create_stop_limit_order(Action.BUY, instrument,
                                                      stopPrice, limitPrice,
                                                      quantity)
        elif quantity < 0:
            ret = self.broker.create_stop_limit_order(Action.SELL, instrument,
                                                      stopPrice, limitPrice,
                                                      quantity * -1)
        if ret:
            ret.good_till_canceled = goodTillCanceled
            ret.all_or_one = allOrNone
            self.broker.submit_order(ret)
        return ret

    def enterLong(self,
                  instrument,
                  quantity,
                  goodTillCanceled=False,
                  allOrNone=False):
        """Generates a buy :class:`pyalgotrade.broker.MarketOrder` to enter a long position.

        :param instrument: Instrument identifier.
        :type instrument: string.
        :param quantity: Entry order quantity.
        :type quantity: int.
        :param goodTillCanceled: True if the entry order is good till canceled. If False then the order gets automatically canceled when the session closes.
        :type goodTillCanceled: boolean.
        :param allOrNone: True if the orders should be completely filled or not at all.
        :type allOrNone: boolean.
        :rtype: The :class:`pyalgotrade.strategy.position.Position` entered.
        """

        return position.LongPosition(self, instrument, None, None, quantity,
                                     goodTillCanceled, allOrNone)

    def enterShort(self,
                   instrument,
                   quantity,
                   goodTillCanceled=False,
                   allOrNone=False):
        """Generates a sell short :class:`pyalgotrade.broker.MarketOrder` to enter a short position.

        :param instrument: Instrument identifier.
        :type instrument: string.
        :param quantity: Entry order quantity.
        :type quantity: int.
        :param goodTillCanceled: True if the entry order is good till canceled. If False then the order gets automatically canceled when the session closes.
        :type goodTillCanceled: boolean.
        :param allOrNone: True if the orders should be completely filled or not at all.
        :type allOrNone: boolean.
        :rtype: The :class:`pyalgotrade.strategy.position.Position` entered.
        """

        return position.ShortPosition(self, instrument, None, None, quantity,
                                      goodTillCanceled, allOrNone)

    def enterLongLimit(self,
                       instrument,
                       limitPrice,
                       quantity,
                       goodTillCanceled=False,
                       allOrNone=False):
        """Generates a buy :class:`pyalgotrade.broker.LimitOrder` to enter a long position.

        :param instrument: Instrument identifier.
        :type instrument: string.
        :param limitPrice: Limit price.
        :type limitPrice: float.
        :param quantity: Entry order quantity.
        :type quantity: int.
        :param goodTillCanceled: True if the entry order is good till canceled. If False then the order gets automatically canceled when the session closes.
        :type goodTillCanceled: boolean.
        :param allOrNone: True if the orders should be completely filled or not at all.
        :type allOrNone: boolean.
        :rtype: The :class:`pyalgotrade.strategy.position.Position` entered.
        """

        return position.LongPosition(self, instrument, None, limitPrice,
                                     quantity, goodTillCanceled, allOrNone)

    def enterShortLimit(self,
                        instrument,
                        limitPrice,
                        quantity,
                        goodTillCanceled=False,
                        allOrNone=False):
        """Generates a sell short :class:`pyalgotrade.broker.LimitOrder` to enter a short position.

        :param instrument: Instrument identifier.
        :type instrument: string.
        :param limitPrice: Limit price.
        :type limitPrice: float.
        :param quantity: Entry order quantity.
        :type quantity: int.
        :param goodTillCanceled: True if the entry order is good till canceled. If False then the order gets automatically canceled when the session closes.
        :type goodTillCanceled: boolean.
        :param allOrNone: True if the orders should be completely filled or not at all.
        :type allOrNone: boolean.
        :rtype: The :class:`pyalgotrade.strategy.position.Position` entered.
        """

        return position.ShortPosition(self, instrument, None, limitPrice,
                                      quantity, goodTillCanceled, allOrNone)

    def enterLongStop(self,
                      instrument,
                      stopPrice,
                      quantity,
                      goodTillCanceled=False,
                      allOrNone=False):
        """Generates a buy :class:`pyalgotrade.broker.StopOrder` to enter a long position.

        :param instrument: Instrument identifier.
        :type instrument: string.
        :param stopPrice: Stop price.
        :type stopPrice: float.
        :param quantity: Entry order quantity.
        :type quantity: int.
        :param goodTillCanceled: True if the entry order is good till canceled. If False then the order gets automatically canceled when the session closes.
        :type goodTillCanceled: boolean.
        :param allOrNone: True if the orders should be completely filled or not at all.
        :type allOrNone: boolean.
        :rtype: The :class:`pyalgotrade.strategy.position.Position` entered.
        """

        return position.LongPosition(self, instrument, stopPrice, None,
                                     quantity, goodTillCanceled, allOrNone)

    def enterShortStop(self,
                       instrument,
                       stopPrice,
                       quantity,
                       goodTillCanceled=False,
                       allOrNone=False):
        """Generates a sell short :class:`pyalgotrade.broker.StopOrder` to enter a short position.

        :param instrument: Instrument identifier.
        :type instrument: string.
        :param stopPrice: Stop price.
        :type stopPrice: float.
        :param quantity: Entry order quantity.
        :type quantity: int.
        :param goodTillCanceled: True if the entry order is good till canceled. If False then the order gets automatically canceled when the session closes.
        :type goodTillCanceled: boolean.
        :param allOrNone: True if the orders should be completely filled or not at all.
        :type allOrNone: boolean.
        :rtype: The :class:`pyalgotrade.strategy.position.Position` entered.
        """

        return position.ShortPosition(self, instrument, stopPrice, None,
                                      quantity, goodTillCanceled, allOrNone)

    def enterLongStopLimit(self,
                           instrument,
                           stopPrice,
                           limitPrice,
                           quantity,
                           goodTillCanceled=False,
                           allOrNone=False):
        """Generates a buy :class:`pyalgotrade.broker.StopLimitOrder` order to enter a long position.

        :param instrument: Instrument identifier.
        :type instrument: string.
        :param stopPrice: Stop price.
        :type stopPrice: float.
        :param limitPrice: Limit price.
        :type limitPrice: float.
        :param quantity: Entry order quantity.
        :type quantity: int.
        :param goodTillCanceled: True if the entry order is good till canceled. If False then the order gets automatically canceled when the session closes.
        :type goodTillCanceled: boolean.
        :param allOrNone: True if the orders should be completely filled or not at all.
        :type allOrNone: boolean.
        :rtype: The :class:`pyalgotrade.strategy.position.Position` entered.
        """

        return position.LongPosition(self, instrument, stopPrice, limitPrice,
                                     quantity, goodTillCanceled, allOrNone)

    def enterShortStopLimit(self,
                            instrument,
                            stopPrice,
                            limitPrice,
                            quantity,
                            goodTillCanceled=False,
                            allOrNone=False):
        """Generates a sell short :class:`pyalgotrade.broker.StopLimitOrder` order to enter a short position.

        :param instrument: Instrument identifier.
        :type instrument: string.
        :param stopPrice: The Stop price.
        :type stopPrice: float.
        :param limitPrice: Limit price.
        :type limitPrice: float.
        :param quantity: Entry order quantity.
        :type quantity: int.
        :param goodTillCanceled: True if the entry order is good till canceled. If False then the order gets automatically canceled when the session closes.
        :type goodTillCanceled: boolean.
        :param allOrNone: True if the orders should be completely filled or not at all.
        :type allOrNone: boolean.
        :rtype: The :class:`pyalgotrade.strategy.position.Position` entered.
        """

        return position.ShortPosition(self, instrument, stopPrice, limitPrice,
                                      quantity, goodTillCanceled, allOrNone)

    @property
    def result(self):
        return self.broker.equity

    @property
    def active_positions(self):
        return self.__activePositions

    @property
    def order_to_positions(self):
        return self.order_to_positions

    @property
    def logger(self):
        return self.__logger

    def debug(self, msg):
        """Logs a message with level DEBUG on the strategy logger."""
        self.logger.debug(msg)

    def info(self, msg):
        """Logs a message with level INFO on the strategy logger."""
        self.logger.info(msg)

    def warning(self, msg):
        """Logs a message with level WARNING on the strategy logger."""
        self.logger.warning(msg)

    def error(self, msg):
        """Logs a message with level ERROR on the strategy logger."""
        self.logger.error(msg)

    def critical(self, msg):
        """Logs a message with level CRITICAL on the strategy logger."""
        self.logger.critical(msg)

    def registerPositionOrder(self, position, order: Order):
        self.__activePositions.add(position)
        assert order.is_active  # Why register an inactive order ?
        self.__orderToPosition[order.id] = position

    def unregisterPositionOrder(self, position, order: Order):
        del self.__orderToPosition[order.id]

    def unregisterPosition(self, position):
        assert (not position.isOpen())
        self.__activePositions.remove(position)

    def __notifyAnalyzers(self, lambdaExpression):
        for s in self.__analyzers:
            lambdaExpression(s)

    def attachAnalyzerEx(self, strategyAnalyzer, name=None):
        if strategyAnalyzer not in self.__analyzers:
            if name is not None:
                if name in self.__namedAnalyzers:
                    raise Exception(
                        "A different analyzer named '%s' was already attached"
                        % name)
                self.__namedAnalyzers[name] = strategyAnalyzer

            strategyAnalyzer.beforeAttachImpl(self)
            self.__analyzers.append(strategyAnalyzer)
            strategyAnalyzer.attached()

    """
        下面开始是事件
    """

    def onEnterOk(self, position):
        """Override (optional) to get notified when the order submitted to enter a position was filled. The default implementation is empty.

        :param position: A position returned by any of the enterLongXXX or enterShortXXX methods.
        :type position: :class:`pyalgotrade.strategy.position.Position`.
        """
        pass

    def onEnterCanceled(self, position):
        """Override (optional) to get notified when the order submitted to enter a position was canceled. The default implementation is empty.

        :param position: A position returned by any of the enterLongXXX or enterShortXXX methods.
        :type position: :class:`pyalgotrade.strategy.position.Position`.
        """
        pass

    # Called when the exit order for a position was filled.
    def onExitOk(self, position):
        """Override (optional) to get notified when the order submitted to exit a position was filled. The default implementation is empty.

        :param position: A position returned by any of the enterLongXXX or enterShortXXX methods.
        :type position: :class:`pyalgotrade.strategy.position.Position`.
        """
        pass

    # Called when the exit order for a position was canceled.
    def onExitCanceled(self, position):
        """Override (optional) to get notified when the order submitted to exit a position was canceled. The default implementation is empty.

        :param position: A position returned by any of the enterLongXXX or enterShortXXX methods.
        :type position: :class:`pyalgotrade.strategy.position.Position`.
        """
        pass

    """Base class for strategies. """

    def onStart(self):
        """Override (optional) to get notified when the strategy starts executing. The default implementation is empty. """
        pass

    def onFinish(self, bars):
        """Override (optional) to get notified when the strategy finished executing. The default implementation is empty.

        :param bars: The last bars processed.
        :type bars: :class:`pyalgotrade.bar.Bars`.
        """
        pass

    def onIdle(self):
        """Override (optional) to get notified when there are no events.

       .. note::
            In a pure backtesting scenario this will not be called.
        """
        pass

    @abc.abstractmethod
    def onBars(self, datetime, bars):
        """Override (**mandatory**) to get notified when new bars are available. The default implementation raises an Exception.

        **This is the method to override to enter your trading logic and enter/exit positions**.

        :param bars: The current bars.
        :type bars: :class:`pyalgotrade.bar.Bars`.
        """
        raise NotImplementedError()

    def onOrderUpdated(self, order):
        """Override (optional) to get notified when an order gets updated.

        :param order: The order updated.
        :type order: :class:`pyalgotrade.broker.Order`.
        """
        pass

    def __onIdle(self):
        # Force a resample check to avoid depending solely on the underlying
        # barfeed events.
        for resampledBarFeed in self.__resampledBarFeeds:
            resampledBarFeed.checkNow(self.current_datetime)

        self.onIdle()

    def __onOrderEvent(self, broker_, orderEvent):
        order = orderEvent.order
        self.onOrderUpdated(order)
        # Notify the position about the order event.
        pos = self.__orderToPosition.get(order.id, None)
        if pos is not None:
            # Unlink the order from the position if its not active anymore.
            if not order.is_active:
                self.unregisterPositionOrder(pos, order)

            pos.onOrderEvent(orderEvent)

    def __appendToSeries(self, bars):
        for instrument, bar in bars.items():
            self.bar_series[instrument].append(bar)

    def __onBars(self, dateTime, bars1, bars2):
        # THE ORDER HERE IS VERY IMPORTANT

        # 0: Append The bar Into the dataseries
        self.__appendToSeries(bars2)
        # 1: Let analyzers process bars.
        self.__notifyAnalyzers(lambda s: s.beforeOnBars(bars2))
        # 2: Let the strategy process current bars and submit orders.
        self.onBars(dateTime, bars2)

        # 3: Notify that the bars were processed.
        self.__barsProcessedEvent.emit(self, bars2)

    def run(self):
        """Call once (**and only once**) to run the strategy."""
        self.__dispatcher.run()
        if self.bar_feed.last_bars is not None:
            self.onFinish(self.bar_feed.last_bars)
        else:
            self.logger.warn('BAR IS EMPTY!')

    def stop(self):
        """Stops a running strategy."""
        self.__dispatcher.stop()

    def attachAnalyzer(self, strategyAnalyzer):
        self.attachAnalyzerEx(strategyAnalyzer)

    def getNamedAnalyzer(self, name):
        return self.__namedAnalyzers.get(name, None)
Example #6
0
class BaseFeed(Subject):
    """Base class for feeds.

    :param maxLen: The maximum number of values that each :class:`myalgo.dataseries.DataSeries` will hold.
        Once a bounded length is full, when new items are added, a corresponding number of items are discarded
        from the opposite end.
    :type maxLen: int.

    .. note::
        This is a base class and should not be used directly.
    """
    def __init__(self, maxLen):
        super(BaseFeed, self).__init__()

        self.__event = Event()
        self.__maxLen = maxLen

    @property
    def max_len(self):
        return self.__maxLen

    def reset(self):
        keys = list(self.__ds.keys())
        self.__ds = {}
        for key in keys:
            self.registerDataSeries(key)

    # Subclasses should implement this and return the appropriate dataseries for the given key.
    @abc.abstractmethod
    def createDataSeries(self, key, maxLen):
        raise NotImplementedError()

    # Subclasses should implement this and return a tuple with two elements:
    # 1: datetime.datetime.
    # 2: dictionary or dict-like object.
    @property
    @abc.abstractmethod
    def next_values(self):
        raise NotImplementedError()

    def getNextValuesAndUpdateDS(self):
        dateTime, values = self.next_values
        return dateTime, values

    def __iter__(self):
        return feed_iterator(self)

    def getNewValuesEvent(self):
        """Returns the event that will be emitted when new values are available.
        To subscribe you need to pass in a callable object that receives two parameters:

         1. A :class:`datetime.datetime` instance.
         2. The new value.
        """
        return self.__event

    def dispatch(self):
        dateTime, values = self.getNextValuesAndUpdateDS()
        if dateTime is not None:
            self.__event.emit(dateTime, values)
        return dateTime is not None

    def getKeys(self):
        return list(self.__ds.keys())

    def __getitem__(self, key):
        return self.__ds[key]

    def __contains__(self, key):
        return key in self.__ds