27 lines
28 KiB
Markdown
27 lines
28 KiB
Markdown
# 05 · Trading & Orders(下单、订单管理与调试)
|
||
|
||
- **系列**:Full Algorithmic Trading Using Python
|
||
- **频道**:TradeOptionsWithMe | **本集**:Trading & Orders(下单、订单管理与调试)
|
||
- **时长**:28 分 46 秒 | **原视频**:https://youtu.be/qkyvj5LIg0M
|
||
- **本地视频**:[videos/05-trading-and-orders.mp4](videos/05-trading-and-orders.mp4)
|
||
|
||
## 🎯 本集要点(中文导读)
|
||
|
||
本集深入 **下单(订单)与调试/日志**。
|
||
|
||
- **Order Ticket(订单票据)**:下单后返回的对象,用来追踪/查询/修改订单。因为订单是**异步**的(更新需请求、不保证成交),所以要靠 ticket 管理。常用属性:`Status`、`OrderId`、`Symbol`、`Quantity`/`QuantityFilled`、`AverageFillPrice`、`OrderType`。
|
||
- **5 种订单类型**:
|
||
- **Market Order**(市价单):`self.MarketOrder(symbol, qty)`;按最优价成交(买=ask、卖=bid,含点差);默认**同步**(最多等 5 秒,可设 `self.Transactions.MarketOrderFillTimeout`),也可异步。
|
||
- **Limit Order** 限价单、**Stop Market Order** 止损市价单、**Stop Limit Order** 止损限价单、**Market On Open/Close**(开盘/收盘市价单)等。
|
||
- 更新/取消订单:先取 ticket,再 `Update`/`Cancel`(仅未成交时有效)。
|
||
- **`self.Transactions`**:访问订单/成交历史。
|
||
- **调试与日志**:`self.Log()`(日志,回测/实盘可见)、`self.Debug()`(控制台);**Log 有容量上限**,别刷太多。还可用平台的 **Debug 模式**(打断点、加 Watch 变量、逐步执行)。
|
||
- **自定义图表**:可绘图(后续课细讲)。
|
||
- 本集用一个示例 bot 演示下单、更新、取消与调试流程。
|
||
|
||
## 📝 完整文稿(英文 · 自动转写)
|
||
|
||
> 由本地 ASR(faster-whisper)从视频音轨转录,未人工校对,供检索/精读使用。原始讲解以上方视频为准。
|
||
|
||
Welcome to the fifth video of my algorithmic trading video course using Python and the QuantConnect platform. If you're new to this series, I recommend first watching the previous videos before continuing with this one. In today's video lesson, we will continue our closer exploration of the QuantConnect API. More specifically, we will dive into how to deal with all kinds of trade orders as well as how to make use of the debugging and logging tools that QuantConnect provides. For this, we will once again be creating a relatively simple trading bot. To make sure you get the most out of your learning, make sure to code along. If you haven't created your free QuantConnect account yet, you can do so using the link in the description box below. But before we jump into the action and start actually coding this trading bot, let's first explore how QuantConnect handles trade orders on a conceptual level. For this, we first look at the concept of order tickets. An order ticket is the object you get after creating an order of any kind. It is used to track, access and edit the given order. The reason why you need an order ticket object instead of just an object for the order itself is that orders are asynchronous. This means that any potential updates first need to be requested and there is no guarantee that they will go through. If for example, you send a limit order for 100 shares of Apple and then decide that you want to console or update this order a few minutes later, you can only do so if the order has not yet been filled. Some of the most interesting properties of an order ticket object are its status, order ID, symbol, quantity, quantity, field, average, field price and order type. But more on some of these later. Now, let's first take a look at how to actually place orders and thereby create ordered tickets in the first place. One connector puts five different order types that we will go over one by one now. The first most simple order type is a standard market order. Market orders are filled at the first available price and they can be sent by using the self.market order method of the QC algorithm class. Self.market order takes two required arguments. The first being the symbol that you want to send a market order for and the second being the order quantity. So a market order for 50 shares of Tesla would look like this. Note that here it would usually be better to use a symbol object instead of a ticker symbol but in most cases both would work. Since market orders usually feel right away, you can't really update them after they are sent. To make things as realistic as possible, QuantConnect does account for the spread. This means buy orders are filled at the asking price and sell orders are filled at the bid price. By default, the market order method is synchronous which means that your algorithm will wait up to five seconds until the order is filled before moving on to the next line of code. It is also possible to customize this timeout delay with self.transactions.market order filled timeout. As a third optional argument, you can also specify whether the market order should be sent asynchronously. Passing true here will mean that your algorithm will not wait before moving on to the next line of code. This can be useful if you are trading large order sizes, sending many orders or trading in illiquid markets and don't want your algorithm to always wait for the entire market order to be filled. The market order method or any other order type method for that matter will return an order ticket object. To save the corresponding ticket, you simply have to save the output to a variable. The next order type is a limit order which allows you to specify a price at which you want your order to be filled at. Limit orders usually take longer to fill or might sometimes not even get filled at all, but on the flip side they allow you to get better entry and exit prices than market orders. You can send a limit order by using self.limit order which takes three arguments. The first two are the symbol and the quantity so the same as for market orders and the third argument is the limit price. Since limit orders can take longer to fill or might not be filled at all, it is possible to update them but more in updating orders in a few minutes. Next up, there are also stock market orders. This is basically a market order that is sent as soon as a predetermined price is reached. This price can be specified as the third argument. So for instance, this stock market order would mean that a market order to sell 200 shares of SPY would be sent as soon as SPY's price drops by 10%, assuming that the SPY close variable represents buy's latest closing price. Note that if prices are very volatile or gapping up or down, a stock market order might be executed at a much worse price than the specified trigger price. Besides stock market orders, the ults are stop limit orders which basically work the same way as stock market orders with the only difference being that a limit order instead of a market order will be sent. Here you can specify the limit price as a fourth argument. Last but not least, the ults are market on open and market on close orders. Like the name implies, these orders are filled at the open or close price of a given security. These orders must be submitted at least two minutes before the open or close respectively. Here you can once again only specify a symbol and a quantity. Note that if you are using daily data or another relatively low resolution, there's a high chance that your market orders will be turned into market on open orders. This is the case because data is submitted at its end time which usually is during market inactivity. So if you send out any market orders based on this info and their market is closed, they will automatically be converted into market on open orders. If you are confused about what I mean with data being submitted on its end time, I recommend watching the third video of this series where I covered the essential topic of understanding how time is being handled inside of the lean trading engine. You might be asking yourself why there aren't any more complex order types such as trading stop-loss orders or contingency orders. Even though QuantConnect does not have a direct helper method for these, you can still create more complex orders out of the aforementioned order types. Later in this video, I will show you an example of how you could go about creating a trailing stop-loss order. Besides the type of order, you can also specify the time-in-force of an order which basically adjusts the length of time that an order stays open until it is filled. Note that the time-in-force can only be adjusted for non-market orders since market orders usually are filled right away. There are three different settings for the time-in-force property. The default value is good till console which means that the order stays open until it is either filled or manually cancelled. The second possible value is day which means that the order will stay active until the end of the day. The final possible value is good till date which allows you to specify a date until the order should remain open. If this day is reached and the order has not yet been filled, it will be cancelled. To change the time-in-force of an order, you have to set the time-in-force value of the default order properties attribute. For instance, changing the time-in-force today would look like this. Next up, let's take a look at how we can go about updating and cancelling orders. Like I already mentioned earlier, depending on the order type, the things you can update can vary. All order types allow you to update the tag property. Tags can be used to mark a certain order ticket for later reference but it has no actual effect on your trades. This table shows which properties you can update for the various order types. Updating the stop price of a stop order while the price of the security rises is one way to create a trailing stop order. To actually update an order, you need to create an update order fields object and then specify the properties that you want to update. You can then pass this object into the update method on the given order tickets. I will show this in a more detailed manner later when we code the actual trading bot. Updating an order returns an order response object which you can use to find out whether the update was successful or not. To cancel an order, you simply code the cancel method on the given order ticket. Note however that you cannot do this for market orders since they are typically filled immediately. Next, let's talk about position sizing. When sending trade orders, you usually want to dynamically adjust their position sizing in relation to the available capital in your portfolio. For example, instead of always sending an order for 100 shares, you'd probably rather want to send an order for 10% of your portfolio's capital. Luckily, QuantConnect has some helper functions to make this easier as well. The first and most commonly used helper function is one that we already used in the last video, namely the sets-holding method. Set-holding allows you to specify what percentage of your portfolio you want to allocate to a given security. Then your algorithm automatically computes the adequate order size and sends out a market order to reach the specified allocation. As an optional third argument, you can pass a Boolean that specifies whether existing holdings should be liquidated before your portfolio capital is allocated. Alternatively, you can use set-holding to scale your holdings up or down to a desired level. For this, you need to pass a list of portfolio target objects like this. Doing so will send out market orders so that your holdings in SPY will be scaled down or up to 80% and those in IBM to 20% of your buying power. If you don't want to use market orders for your trading, you can't use the set-holding cell per function. However, there's still another way to intelligently calculate order sizes. For this, you can use self.calculate order quantity which takes two arguments. The first is a symbol and the second is a percentage. This will then return an integer which represents the number of shares of the specified security that would make up the specified percentage of your available buying power. So for instance, this is how you could send a limit order that allocates 30% of your buying power to Apple at its current market price. Sometimes it can be useful to have a buffer of cash in your account so that you don't suddenly run out of buying power. To ensure that you always have enough cash reserves, you can use self.settings.free portfolio value percentage and set it to a percentage of your portfolio that you want to keep in cash. An easy way to close positions is using self.liquidate. Without any argument, self.liquidate will simply liquidate all your holdings using market orders. Alternatively, you can pass a specific symbol to only close positions in this security. Before we finally move on to the actual coding part of this video, there are few more things I want to cover. One of them is the unorder event method. This is an event handler that is automatically called every time an order event such as a change in the status of an order occurs. This is a great way to check when your orders are being filled and potentially act upon this information as soon as you have it. All order status includes submitted, filled, partially filled, cancelled, invalid and others. We will cover this in more detail once we start coding in a few minutes. Besides accessing orders through order events, you can also do so through your algorithm's transaction manager. For example, self.transactions.getOpenOrders allows you to get all the open orders for a specified symbol. Furthermore, you can use self.transactions.getOrderByID to get the orders by their order ID. Another helper method that the transactions manager provides is CancelOpenOrders, which allows you to cancel all open orders for security or the entire portfolio. This can be very useful in a scenario where you would want to stop all trading activity. Last but not least, QuantConnect also has the option for you to select from various different fee and brokerage models to model trading costs as realistically as possible. You can also create your own custom slipper and other transaction cost models, but since this is a more advanced topic, this is something we might cover later in this series. That said, let us now finally start putting some of the things learned here into practice by writing some code. Just like last time, let me quickly present the trading strategy that we will implement on a theoretical level before we start actually writing the code. Once again, the strategy that we will be implementing is a very simple strategy and I would not recommend trading it with real money. Its main purpose is to show how orders are being handled inside of QuantConnect. The idea behind this strategy is to buy and hold a given stock or ETF. We then create a trading stop-loss order that trails the stock's price 5% below its price. This hopefully allows us to keep our losers relatively small while leaving the winners room to go up. If our stop-loss is hit, we exit the position and then we wait for one month before we start investing again. If we wouldn't wait for some time before investing again, the trailing stop would be quite useless since we will just immediately re-buy after closing the position. For this video, we will be using the ETF QQQ for our trading, but feel free to try out different stocks or other ETFs. That said, let's now head over to the QuantConnect platform and start implementing the code for this strategy. Out of the QuantConnect platform, we will create a new algorithm inside of the lab tab. Since we don't need the strategy builder, we will exit the builder mode and start with a blank template algorithm. As always, we will then start by implementing the initialize method. This is where we will set a few backtesting settings, add the necessary data and define some help for variables that we will need later. First off, we start by setting the start and end dates for our backtest. Here you can choose whatever time frame you want to. I'll just go for 2018 until 2021. After that, we set the start and cache balance for this backtest. Remember that this is only for backtesting. In real trading, this will be your account balance. Thereafter, we want to add the data for QQQQ, which is the security that this algorithm will be trading. For this, we can use the add equity method and pass the QQQ ticker symbol as well as a resolution. Here we will go with hourly resolution, but you could also use daily or another resolution. We then save the symbol object of QQQQ to the variable self.qqqq, which we will use to reference this security in our algorithm. Now all that's left to do in the initialize method is define and initialize a couple of helper variables. Here, we first create two variables that we will use to access the order tickets of our entry and exit orders. We call the first variable, entry ticket, and the second one, stop market ticket, and we initialize both the none. Then we create two more variables to track the field time of the entry and exit orders. This will be used to make sure that we wait 30 days before starting to invest again. Here we call the first variable, entry time, and the second, stop market field time. We initialize both for the earliest possible date since no orders have been filled yet. First but not least, we create the helper variable self.highestprize, which unsurprisingly keeps track of QQQ's highest price. We need this for our trailing stop loss later. With that, we are now done with the initialize method and can move on to the on-data method. This is called every time the algorithm receives new data. For a more detailed breakdown of how data flows inside of the QC algorithm class, make sure to check out some of the previous videos of this series. Before we write the actual Python code, let's sketch out what we want to do with a few comments. The first thing we want to do in the on-data method is check whether 30 days have passed since we closed the last position. Then we want to send an entry limit order for as many shares of QQQQ as we can buy. Since we will be using limit orders, there's no guarantee that they will be filled. If the limit order is not filled within one day, we will move up the limit price so that we will increase the chance of getting filled. The final thing that we want to do in the on-data method is move up the price of the trailing stop loss if necessary. You might have noticed that we don't actually send out any stop loss order anywhere in the on-data method. That's because we do this in the on-order event method. This event handler is called on every order event and this is where we want to send out the stop loss order. However, we only want to do this if the entry limit orders feel it. Last but not least, we want to save the current time in case our stop loss orders feel it. We need to do this so we correctly stop investing for 30 days after closing our position. That's basically what our algorithm will do, so all that's left to do now is code this out and then we're good to go. I will start with the on-data method. Waiting 30 days can be accomplished with a simple if statement. For this, we simply check if the difference between the current time and the stop market order feel time is less than 30 days. If it is, it's not time to invest again and we return. Otherwise we move on. For the rest of the method, we will need QQQ's price. As you hopefully can remember from last video, there are multiple ways to access QQQ's price. We will just index the self.securities dictionary and save the most recent price to the price variable. Then we want to send the limit order. However, we only want to do this if we aren't currently invested and there aren't already any other active orders for QQQ position. We can check this with portfolio.invested and transactions.getOpenOrders respectively. If there aren't any open orders, transactions.getOpenOrders will be an empty list and evaluate to false. If this condition is fulfilled, we first want to calculate the order quantity. For this we can use the helper function calculate order quantity. Here we pass self.qqq as the symbol at 90% as the allocation of our portfolio. This will then calculate the number of shares that we need to buy to achieve a 90% allocation of our portfolio to QQQQ. With this, we can then send a limit order with self.limitOrder. As an optional fourth argument, we can pass a tag for this order. For the sake of showing this, I will tag this order with entry order. Since we might want to update this order later, we will save its order ticket to the entry ticket variable. Furthermore, we will save the current time to the entry time variable. The next step is to move the limit price in case the limit order is not filled after one day. For this, we first check whether more than one day has passed and then we check whether the limit order has been filled or not. We can check this by accessing the status property of the entry ticket order ticket. If it has not been filled, we want to update the entry time to the current time as well as update the limit price. To update the limit price, we create an update order fields object. Then we set the limit price of this update order fields object to the current price of QQQQ. Finally, we can then update the limit order by passing the update order fields object to the update method on our entry ticket order ticket object. Now what that left to do in the on data method is update the trailing stop loss price when QQQQ's price reaches a new high. To accomplish this, we first must check if the stop market ticket object is not empty and that we are invested. This ensures that we currently have an active stop market order for our QQQQ position. If this is the case, we want to check if QQQ's most recent price is higher than our previously saved highest price. In that case, we first need to update the save.highest price variable to account for this new high. Then we once again create an update order fields object. This time however, we will set the stop price to a new value, namely to 5% below the most recent price which should be the same as QQQQ's highest price. Thereafter, we just have to update the stop market ticket and then we are already done with the on data method. Now all that left to do is add a few lines of code to the on order event method. Inside of the on order event method, we will first check if the status of the submitted order event equals filled. Other possible statuses include submitted, invalid, partially filled among others. But we are only interested in the cases where an order has been filled. That's why we return if this is not the case. After that, the first case we want to cover is the case in which the entry order has been filled. To check this, we first check that the entry ticket variable is not none and that the order ID of the current order event in fact equals the order ID of this entry ticket object. In that case, we know that our entry limit order has been filled. That's why we can now send a stop market order for our QQQ shares. For the quantity, we can just check the quantity of our entry ticket object since we want a stop market order for the entire position. Note that we pass a negative value here since this is a settled order. The third argument of the stop market order method is the stop price. We want this to be 5% below the average filled price of our entry order. After this, the only other case that we need to handle is the case in which the stop market order has been filled. To check this, we once again have to check whether the stop market order ticket object is non-empty and the order ID of the order event parameter is correct. In that case, we simply want to update the stop market order filled time variable to the current time. This is important since this ensures that we will wait 30 days starting now. Last but not least, we reset the self dot highest price variable to zero since we don't know if QQQ's price will be lower in a month from now. With that, we now have successfully implemented this trading bot. Now we can click on the build button and then on the back test button to test this bot over the specified time period. Since this is a relatively simple bot, the back testing should not take very long. When the back test is finished, you will see a performance overview like this one. I don't want to go over this in any detail since the point of this video is not to create a particularly well performing bot. I will cover how to use this report to analyze the performance of a strategy later in this series. For now, I just want to scroll down to the orders tab so that we can make sure that this bot is in fact doing what it's supposed to do. Even for relatively simple bots, it can be very helpful to look at the orders that are being sent to get a better understanding of what it's doing. This can be especially helpful when your bot is doing something it's not supposed to do and you're trying to find out why. Here we can see an overview of all orders that have been sent. If we click on the orders, we can see a more detailed breakdown of them. In the rightmost column, you can see the custom order tag that we submitted for the entry limit orders. As you can see, the first order here was a limit order for 584 shares of QQQ with a limit price of about $153.5. This limit order then got filled pretty much straight away. In hour after the limit order was sent, the stop plus order with an initial stop price of almost $146 was sent out. Then, we can see a bunch of updates of the stop price while QQQ's price increased until the stop order was filled on the 5th of February at a little over $157. The next entry order was first sent about a month after this field time, which is exactly how we want it to be. So judging from these few orders, the algorithm seems to be doing what it's supposed to do. Besides analyzing the orders to ensure you fully understand your algorithm, there are a few other debugging and logging tools that QQQ provides. One of the easiest ways to double check certain things is by using self.debog. This is similar to Python's print statement. If for instance, you want to make sure that the trading stop plus price is being updated correctly, you could use self.debog to print the updated stop price to the console. For this, you would then have to restart the backtest and look at the values printed to the console at the bottom of the page. Besides self.debog, there also is self.error which prints a red message to the console. Note that if the same values are being printed over and over again, QQQQ will limit the printing rate to prevent browser flooding. Finally, you can also use self.log to log certain actions. These logs can then be accessed when the backtest is finished at the bottom of the backtest report. These logs do come with a limited capacity however, so you can't generate megabytes of logs for every backtest. Another great way to improve the understanding of your algorithm is by creating custom charts and plotting certain values. But that's something you will learn in one of the next few videos. If all that doesn't help, QQQQ also has a debugging mode which you can access by clicking on debug on the right hand side of your code. Like in other ideas, you can set break points to the left of your code to incrementally step through your code. Besides break points, you can also add variables or other expressions to a watchlist. As an example, let me quickly set a breakpoint on this if condition and let's add self.highest price to the watchlist. If we now click on backtest, the algorithm will stop every time we reach this if condition. Furthermore, you can see how the highest price changes over time. Incrementally stepping through your code and analyzing certain variables can be very useful when looking for bugs. Note that besides watching simple variables, you can also track more complex expressions such as logical expressions, lists and much more. In my opinion, the best way to get familiar with the debugging mode is by just trying it out and playing around with it. That being said, this was quite the long video, so I hope it wasn't too overwhelming and you learned something new. If you thought a few things went over your head, don't worry, you can always rewatch parts of this video or even the entire thing. In the next video, we will take a look at how indicators work, how to create your own indicators and much more. If you enjoyed this video, definitely make sure to subscribe, turn on the notification bell and smash the like button for more content like this. Thanks for watching.
|