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 __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 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)
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)
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)
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
def __init__(self): super(ReturnsAnalyzerBase, self).__init__() self.__event = Event() self.__portfolioReturns = None
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
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
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)
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)
def __init__(self, maxLen): super(BaseFeed, self).__init__() self.__event = Event() self.__maxLen = maxLen
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