Repository: alexgolec/tda-api

Issue:
How to create complex orders?
How do you create complex/multi-leg orders? 

E.g. OCO (one cancels other), or with multiple option legs. 

And how do you create order types other than limit/market? E.g. stop loss, trailing stop, etc.

There are examples of these on the TD Ameritrade api site, but I can't figure if these are built into the library, or if there is a workaround. If they are not available, is it planned for a future release?

Discussion:

Patch:
diff --git a/docs/client.rst b/docs/client.rst
index 6afeeff..dc13453 100644
--- a/docs/client.rst
+++ b/docs/client.rst
@@ -106,6 +106,9 @@ own, the constructor looks like this:
 Orders
 ++++++
 
+
+.. _placing_new_orders:
+
 ------------------
 Placing New Orders
 ------------------
diff --git a/docs/index.rst b/docs/index.rst
index aab2a53..e7f49cb 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -17,7 +17,8 @@
   auth
   client
   streaming
-  orders
+  order-templates
+  order-builder
   util
   example
   help
diff --git a/docs/order-builder.rst b/docs/order-builder.rst
new file mode 100644
index 0000000..123b3f4
--- /dev/null
+++ b/docs/order-builder.rst
@@ -0,0 +1,475 @@
+.. py:module:: tda.orders.generic
+
+.. _order_builder:
+
+==========================
+``OrderBuilder`` Reference
+==========================
+
+The :meth:`Client.place_order() <tda.client.Client.place_order>` method expects 
+a rather complex JSON object that describes the desired order. TDA provides some 
+`example order specs 
+<https://developer.tdameritrade.com/content/place-order-samples>`__ to
+illustrate the process and provides a schema in the `place order documentation 
+<https://developer.tdameritrade.com/account-access/apis/post/accounts/
+%7BaccountId%7D/orders-0>`__, but beyond that we're on our own. ``tda-api`` aims 
+to be useful to everyone, from users who want to easily place common equities 
+and options trades, to advanced users who want to place complex multi-leg, 
+multi-asset type trades. 
+
+For users interested in simple trades, ``tda-api`` supports pre-built 
+:ref:`order_templates` that allow fast construction of many common trades. 
+Advanced users can modify these trades however they like, and can even build 
+trades from scratch.
+
+This page describes the features of the complete order schema in all their 
+complexity. It is aimed at advanced users who want to create complex orders. 
+Less advanced users can use the :ref:`order templates <order_templates>` to 
+create orders. If they find themselves wanting to go beyond those templates, 
+they can return to this page to learn how.
+
+
+------------------------------------------
+Optional: Order Specification Introduction
+------------------------------------------
+
+Before we dive in to creating order specs, let's briefly introduce their 
+structure. This section is optional, although users wanting to use more advanced 
+featured like stop prices and complex options orders will likely want to read it.
+
+Here is an example of a spec that places a limit order to buy 13 shares of
+``MSFT`` for no more than $190. This is exactly the order that would be returned 
+by :func:`tda.orders.equities.equity_buy_limit`:
+
+.. code-block:: JSON
+
+  {
+      "session": "NORMAL",
+      "duration": "DAY",
+      "orderType": "LIMIT",
+      "price": "190.90",
+      "orderLegCollection": [
+          {
+              "instruction": "BUY",
+              "instrument": {
+                  "assetType": "EQUITY",
+                  "symbol": "MSFT"
+              },
+              "quantity": 1
+          }
+      ],
+      "orderStrategyType": "SINGLE"
+  }
+
+Some key points are:
+
+ * The ``LIMIT`` order type notifies TD that you'd like to place a limit order.
+ * The order strategy type is ``SINGLE``, meaning this order is not a composite 
+   order.
+ * The order leg collection contains a single leg to purchase the equity.
+ * The price is specified *outside* the order leg. This may seem 
+   counterintuitive, but it's important when placing composite options orders.
+
+If this seems like a lot of detail to specify a rather simple order, it is. The 
+thing about the order spec object is that it can express *every* order that can 
+be made through the TD Ameritrade API. For an advanced example, here is a order 
+spec for a standing order to enter a long position in ``GOOG`` at $1310 or less 
+that triggers a one-cancels-other order that exits the position if the price 
+rises to $1400 or falls below $1250:
+
+.. code-block:: JSON
+
+  {
+      "session": "NORMAL",
+      "duration": "GOOD_TILL_CANCEL",
+      "orderType": "LIMIT",
+      "price": "1310.00",
+      "orderLegCollection": [
+          {
+              "instruction": "BUY",
+              "instrument": {
+                  "assetType": "EQUITY",
+                  "symbol": "GOOG"
+              },
+              "quantity": 1
+          }
+      ],
+      "orderStrategyType": "TRIGGER",
+      "childOrderStrategies": [
+          {
+              "orderStrategyType": "OCO",
+              "childOrderStrategies": [
+                  {
+                      "session": "NORMAL",
+                      "duration": "GOOD_TILL_CANCEL",
+                      "orderType": "LIMIT",
+                      "price": "1400.00",
+                      "orderLegCollection": [
+                          {
+                              "instruction": "SELL",
+                              "instrument": {
+                                  "assetType": "EQUITY",
+                                  "symbol": "GOOG"
+                              },
+                              "quantity": 1
+                          }
+                      ]
+                  },
+                  {
+                      "session": "NORMAL",
+                      "duration": "GOOD_TILL_CANCEL",
+                      "orderType": "STOP_LIMIT",
+                      "stopPrice": "1250.00",
+                      "orderLegCollection": [
+                          {
+                              "instruction": "SELL",
+                              "instrument": {
+                                  "assetType": "EQUITY",
+                                  "symbol": "GOOG"
+                              },
+                              "quantity": 1
+                          }
+                      ]
+                  }
+              ]
+          }
+      ]
+  }
+
+While this looks complex, it can be broken down into the same components as the 
+simpler buy order:
+
+ * This time, the ``LIMIT`` order type applies to the top-level order.
+ * The order strategy type is ``TRIGGER``, which tells TD Ameritrade to hold off 
+   placing the the second order until the first one completes.
+ * The order leg collection still contains a single leg, and the price is still 
+   defined outside the order leg. This is typical for equities orders.
+
+There are also a few things that aren't there in the simple buy order:
+
+ * The ``childOrderStrategies`` contains the ``OCO`` order that is triggered 
+   when the first ``LIMIT`` order is executed. 
+ * If you look carefully, you'll notice that the inner ``OCO`` is a 
+   fully-featured suborder in itself. 
+
+This order is large and complex, and it takes a lot of reading to understand 
+what's going on here. Fortunately for you, you don't have to; ``tda-api`` cuts 
+down on this complexity by providing templates and helpers to make building 
+orders easy:
+
+.. code-block:: python
+
+  from tda.orders.common import OrderType
+  from tda.orders.generic import OrderBuilder
+
+  one_triggers_other(
+      equity_buy_limit('GOOG', 1, 1310),
+      one_cancels_other(
+          equity_sell_limit('GOOG', 1, 1400),
+          equity_sell_limit('GOOG', 1, 1240)
+              .set_order_type(OrderType.STOP_LIMIT)
+              .clear_price()
+              .set_stop_price(1250)
+      )
+
+You can find the full listing of order templates and utility functions 
+:ref:`here <order_templates>`.
+
+Now that you have some background on how orders are structured, let's dive into 
+the order builder itself. 
+
+
+--------------------------
+``OrderBuilder`` Reference
+--------------------------
+
+This section provides a detailed reference of the generic order builder. You can 
+use it to help build your own custom orders, or you can modify the pre-built 
+orders generated by ``tda-api``'s order templates.
+
+Unfortunately, this reference is largely reverse-engineered. It was initially 
+generated from the schema provided in the `official API documents
+<https://developer.tdameritrade.com/account-access/apis/post/accounts/
+%7BaccountId%7D/orders-0>`__, but many of the finer points, such as which fields 
+should be populated for which order types, etc. are best guesses. If you find 
+something is inaccurate or missing, please `let us know 
+<https://github.com/alexgolec/tda-api/issues>`__.
+
+That being said, experienced traders who understand how various order types and 
+complex strategies work should find this builder easy to use, at least for the 
+order types with which they are familiar. Here are some resources you can use to 
+learn more, courtesy of the Securites and Exchange Commission:
+
+ * `Trading Basics: Understanding the Different Ways to Buy and Sell Stock 
+   <https://www.sec.gov/investor/alerts/trading101basics.pdf>`__
+ * `Trade Execution: What Every Investor Should Know <https://www.sec.gov/
+   reportspubs/investor-publications/investorpubstradexechtm.html>`__
+ * `Investor Bulletin: An Introduction to Options <https://www.sec.gov/oiea/
+   investor-alerts-bulletins/ib_introductionoptions.html>`__
+
+You can also find TD Ameritrade's official documentation on orders `here
+<https://www.tdameritrade.com/retail-en_us/resources/pdf/SDPS819.pdf>`__, 
+although it doesn't actually cover all functionality that ``tda-api`` supports.
+
+
++++++++++++
+Order Types
++++++++++++
+
+Here are the order types that can be used:
+
+.. autoclass:: tda.orders.common::OrderType
+  :members:
+  :undoc-members:
+.. automethod:: tda.orders.generic.OrderBuilder.set_order_type
+.. automethod:: tda.orders.generic.OrderBuilder.clear_order_type
+
+
+++++++++++++++++++++
+Session and Duration
+++++++++++++++++++++
+
+Together, these fields control when the order will be placed and how long it 
+will remain active. Note ``tda-api``'s :ref:`templates <order_templates>` place 
+orders that are active for the duration of the current normal trading session. 
+If you want to modify the default session and duration, you can use these 
+methods to do so.
+
+.. autoclass:: tda.orders.common::Session
+  :members:
+  :undoc-members:
+.. autoclass:: tda.orders.common::Duration
+  :members:
+  :undoc-members:
+
+.. automethod:: tda.orders.generic.OrderBuilder.set_duration
+.. automethod:: tda.orders.generic.OrderBuilder.clear_duration
+.. automethod:: tda.orders.generic.OrderBuilder.set_session
+.. automethod:: tda.orders.generic.OrderBuilder.clear_session
+
+
++++++
+Price
++++++
+
+Price is the amount you'd like to pay for each unit of the position you're 
+taking:
+
+ * For equities and simple options limit orders, this is the price which you'd 
+   like to pay/receive. 
+ * For complex options limit orders (net debit/net credit), this is the total
+   credit or debit you'd like to receive.
+
+In other words, the price is the sum of the prices of the :ref:`order_legs`. 
+This is particularly powerful for complex multi-leg options orders, which 
+support complex top and/or limit orders that trigger when the price of a 
+position reaches certain levels. In those cases, the price of an order can drop 
+below the specified price as a result of movements in multiple legs of the 
+trade. 
+
+.. automethod:: tda.orders.generic.OrderBuilder.set_price
+.. automethod:: tda.orders.generic.OrderBuilder.clear_price
+
+
+.. _order_legs:
+
+++++++++++
+Order Legs
+++++++++++
+
+Order legs are where the actual assets being bought or sold are specified. For 
+simple equity or single-options orders, there is just one leg. However, for 
+complex multi-leg options trades, there can be more than one leg.
+
+Note that order legs often do not execute all at once. Order legs can be 
+executed over the specified :class:`~tda.orders.common.Duration` of the order. 
+What's more, if order legs request a large number of shares, legs themselves can 
+be partially filled. You can control this setting using the 
+:class:`~tda.orders.common.SpecialInstruction` value ``ALL_OR_NONE``. 
+
+With all that out of the way, order legs are relatively simple to specify. 
+``tda-api`` currently supports equity and option order legs:
+
+.. automethod:: tda.orders.generic.OrderBuilder.add_equity_leg
+.. autoclass:: tda.orders.common::EquityInstruction
+  :members:
+  :undoc-members:
+
+.. automethod:: tda.orders.generic.OrderBuilder.add_option_leg
+.. autoclass:: tda.orders.common::OptionInstruction
+  :members:
+  :undoc-members:
+
+.. automethod:: tda.orders.generic.OrderBuilder.clear_order_legs
+
+
+
++++++++++++++++++++++
+Requested Destination
++++++++++++++++++++++
+
+By default, TD Ameritrade sends trades to whichever exchange provides the best 
+price. This field allows you to request a destination exchange for your trade, 
+although whether your order is actually executed there is up to TDA.
+
+.. autoclass:: tda.orders.common::Destination
+  :members:
+  :undoc-members:
+.. automethod:: tda.orders.generic.OrderBuilder.set_requested_destination
+.. automethod:: tda.orders.generic.OrderBuilder.clear_requested_destination
+
+
+++++++++++++++++++++
+Special Instructions
+++++++++++++++++++++
+
+Trades can contain special instructions which handle some edge cases:
+
+.. autoclass:: tda.orders.common::SpecialInstruction
+  :members:
+  :undoc-members:
+.. automethod:: tda.orders.generic.OrderBuilder.set_special_instruction
+.. automethod:: tda.orders.generic.OrderBuilder.clear_special_instruction
+
+
+++++++++++++++++++++++++++
+Complex Options Strategies
+++++++++++++++++++++++++++
+
+TD Ameritrade supports a number of complex options strategies. These strategies 
+are complex affairs, with each leg of the trade specified in the order legs. TD 
+performs additional validation on these strategies, so they are somewhat 
+complicated to place. However, the benefit is more flexibility, as trades like 
+trailing stop orders based on net debit/credit can be specified.
+
+Unfortunately, due to the complexity of these orders and the lack of any real 
+documentation, we cannot offer definitively say how to structure these orders. A 
+few things have been observed, however:
+
+ * The legs of the order can be placed by adding them as option order legs using
+   :meth:`~tda.orders.generic.OrderBuilder.add_option_leg`.
+ * For spreads resulting in a new debit/credit, the price represents the overall 
+   debit or credit desired.
+
+If you successfully use these strategies, we want to know about it. Please let 
+us know by joining our `Discord server <https://discord.gg/nfrd9gh>`__ to chat 
+about it, or by `creating a feature request <https://github.com/alexgolec/
+tda-api/issues>`__.
+
+.. autoclass:: tda.orders.common::ComplexOrderStrategyType
+  :members:
+  :undoc-members:
+.. automethod:: tda.orders.generic.OrderBuilder.set_complex_order_strategy_type
+.. automethod:: tda.orders.generic.OrderBuilder.clear_complex_order_strategy_type
+
+
+++++++++++++++++
+Composite Orders
+++++++++++++++++
+
+``tda-api`` supports composite order strategies, in which execution of one order 
+has an effect on another:
+
+ * ``OCO``, or "one cancels other" orders, consist of a pair of orders where 
+   execution of one immediately cancels the other.
+ * ``TRIGGER`` orders consist of a pair of orders where execution of one 
+   immediately results in placement of the other.
+
+``tda-api`` provides helpers to specify these easily: 
+:func:`~tda.orders.common.one_cancels_other` and
+:func:`~tda.orders.common.first_triggers_second`. This is almost certainly 
+easier than specifying these orders manually. However, if you still want to 
+create them yourself, you can specify these composite order strategies like so:
+
+.. autoclass:: tda.orders.common::OrderStrategyType
+  :members:
+  :undoc-members:
+.. automethod:: tda.orders.generic.OrderBuilder.set_order_strategy_type
+.. automethod:: tda.orders.generic.OrderBuilder.clear_order_strategy_type
+
+
++++++++++++++++++++
+Undocumented Fields
++++++++++++++++++++
+
+Unfortunately, your humble author is not an expert in all things trading. The 
+order spec schema describes some things that are outside my ability to document, 
+so rather than make stuff up, I'm putting them here in the hopes that someone 
+will come along and shed some light on them. You can make suggestions by filing 
+an issue on our 
+`GitHub issues page <https://github.com/alexgolec/tda-api/issues>`__, 
+or by joining our `Discord server <https://discord.gg/nfrd9gh>`__.
+
+
+.. _undocumented_quantity:
+
+~~~~~~~~
+Quantity
+~~~~~~~~
+
+This one seems obvious: doesn't the quantity mean the number of stock I want to 
+buy? The trouble is that the order legs also have a ``quantity`` field, which 
+suggests this field means something else. The leading hypothesis is that is 
+outlines the number of copies of the order to place, although we have yet to 
+verify that.
+
+.. automethod:: tda.orders.generic.OrderBuilder.set_quantity
+.. automethod:: tda.orders.generic.OrderBuilder.clear_quantity
+
+
+~~~~~~~~~~~~~~~~~~~~~~~~
+Stop Order Configuration
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Stop orders and their variants (stop limit, trailing stop, trailing stop limit) 
+support some rather complex configuration. Both stops prices and the limit 
+prices of the resulting order can be configured to follow the market in a 
+dynamic fashion. The market dimensions that they follow can also be configured 
+differently, and it appears that which dimensions are supported varies by order 
+type. 
+
+We have unfortunately not yet done a thorough analysis of what's supported, nor 
+have we made the effort to make it simple and easy. While we're *pretty* sure we 
+understand how these fields work, they've been temporarily placed into the 
+"undocumented" section, pending a followup. Users are invited to experiment with 
+these fields at their own risk. 
+
+
+.. automethod:: tda.orders.generic.OrderBuilder.set_stop_price
+.. automethod:: tda.orders.generic.OrderBuilder.clear_stop_price
+
+.. autoclass:: tda.orders.common::StopPriceLinkBasis
+  :members:
+  :undoc-members:
+.. automethod:: tda.orders.generic.OrderBuilder.set_stop_price_link_basis
+.. automethod:: tda.orders.generic.OrderBuilder.clear_stop_price_link_basis
+
+.. autoclass:: tda.orders.common::StopPriceLinkType
+  :members:
+  :undoc-members:
+.. automethod:: tda.orders.generic.OrderBuilder.set_stop_price_link_type
+.. automethod:: tda.orders.generic.OrderBuilder.clear_stop_price_link_type
+
+.. automethod:: tda.orders.generic.OrderBuilder.set_stop_price_offset
+.. automethod:: tda.orders.generic.OrderBuilder.clear_stop_price_offset
+
+.. autoclass:: tda.orders.common::StopType
+  :members:
+  :undoc-members:
+.. automethod:: tda.orders.generic.OrderBuilder.set_stop_type
+.. automethod:: tda.orders.generic.OrderBuilder.clear_stop_type
+
+.. autoclass:: tda.orders.common::PriceLinkBasis
+  :members:
+  :undoc-members:
+.. automethod:: tda.orders.generic.OrderBuilder.set_price_link_basis
+.. automethod:: tda.orders.generic.OrderBuilder.clear_price_link_basis
+
+.. autoclass:: tda.orders.common::PriceLinkType
+  :members:
+  :undoc-members:
+.. automethod:: tda.orders.generic.OrderBuilder.set_price_link_type
+.. automethod:: tda.orders.generic.OrderBuilder.clear_price_link_type
+
+.. automethod:: tda.orders.generic.OrderBuilder.set_activation_price
+.. automethod:: tda.orders.generic.OrderBuilder.clear_activation_price
diff --git a/docs/order-templates.rst b/docs/order-templates.rst
new file mode 100644
index 0000000..64d5c74
--- /dev/null
+++ b/docs/order-templates.rst
@@ -0,0 +1,128 @@
+.. py:module:: tda.orders.templates
+
+.. _order_templates:
+
+===============
+Order Templates
+===============
+
+``tda-api`` strives to be easy to use. This means making it easy to do simple 
+things, while making it possible to do complicated things. Order construction is
+a major challenge to this mission: both simple and complicated orders use the 
+same format, meaning simple orders require a surprising amount of sophistication 
+to place. 
+
+We get around this by providing templates that make it easy to place common 
+orders, while allowing advanced users to modify the orders returned from the 
+templates to create more complex ones. Very advanced users can even create their 
+own orders from scratch. This page describes the simple templates, while the 
+:ref:`order_builder` page documents the order builder in all its complexity. 
+
+
+---------------------
+Using These Templates
+---------------------
+
+These templates serve two purposes. First, they are designed to choose defaults 
+so you can immediately :ref:`place them <placing_new_orders>`. These defaults 
+are:
+
+ * All orders execute during the current normal trading session. If placed 
+   outside of trading hours, the execute during the next normal trading session.
+ * Time-in-force is set to ``DAY``.
+ * All other fields (such as requested destination, etc.) are left unset, 
+   meaning they receive default treatment from TD Ameritrade. Note this 
+   treatment depends on TDA's implementation, and may change without warning.
+
+Secondly, they serve as starting points for building more complex order types. 
+All templates return a pre-populated ``OrderBuilder`` object, meaning complex 
+functionality can be specified by modifying the returned object. For example, 
+here is how you would place an order to buy ``GOOG`` for no more than $1250 at 
+any time in the next six months:
+
+.. code-block:: python
+
+  from tda.orders.equities import equity_buy_limit
+  from tda.orders.common import Duration, Session
+
+  (equity_buy_limit('GOOG', 1, 1250.0)
+   .set_duration(Duration.GOOD_TILL_CANCEL)
+   .set_session(Session.SEAMLESS))
+
+You can find a full reference for all supported fields in :ref:`order_builder`.
+
+
+----------------
+Equity Templates
+----------------
+
+++++++++++
+Buy orders
+++++++++++
+
+.. autofunction:: tda.orders.equities.equity_buy_market
+.. autofunction:: tda.orders.equities.equity_buy_limit
+
++++++++++++
+Sell orders
++++++++++++
+
+.. autofunction:: tda.orders.equities.equity_sell_market
+.. autofunction:: tda.orders.equities.equity_sell_limit
+
++++++++++++++++++
+Sell short orders
++++++++++++++++++
+
+.. autofunction:: tda.orders.equities.equity_sell_short_market
+.. autofunction:: tda.orders.equities.equity_sell_short_limit
+
++++++++++++++++++++
+Buy to cover orders
++++++++++++++++++++
+
+.. autofunction:: tda.orders.equities.equity_buy_to_cover_market
+.. autofunction:: tda.orders.equities.equity_buy_to_cover_limit
+
+
+-----------------
+Options Templates
+-----------------
+
+Due to their complexity, options order templates are pending. Templates for 
+options orders will be added in subsequent releases. 
+
+In the meantime, you can construct all supported options orders using the 
+:ref:`OrderBuilder <order_builder>`, although you will have to construct them 
+yourself.
+
+---------------
+Utility Methods
+---------------
+
+These methods return orders that represent complex multi-order strategies, 
+namely "one cancels other" and "first triggers second" strategies. Note they 
+expect all their parameters to be of type ``OrderBuilder``. You can construct 
+these orders using the templates above or by 
+:ref:`creating them from scratch <order_builder>`.
+
+.. autofunction:: tda.orders.common.one_cancels_other
+.. autofunction:: tda.orders.common.first_triggers_second
+
+
+----------------------------------------
+What happened to ``EquityOrderBuilder``?
+----------------------------------------
+
+Long-time users may notice that this documentation no longer mentions the 
+``EquityOrderBuilder`` class. This class used to be used to create equities 
+orders, and offered a subset of the functionality offered by the 
+:ref:`OrderBuilder <order_builder>`. This class has been deprecated in favor of 
+the order builder and the above templates, and will be removed from a future 
+release. 
+
+In the meantime, you can continue using this order builder, although you really 
+should migrate to the new one soon. You can find documentation for this class in 
+the `older versions 
+<https://tda-api.readthedocs.io/en/v0.3.2/orders.html>`__ of ``tda-api``'s 
+documentation. 
diff --git a/docs/orders.rst b/docs/orders.rst
deleted file mode 100644
index 065c755..0000000
--- a/docs/orders.rst
+++ /dev/null
@@ -1,57 +0,0 @@
-.. _orders:
-
-=============================
-Creating Order Specifications
-=============================
-
-The :meth:`Client.place_order() <tda.client.Client.place_order>` method expects 
-a rather complex JSON object that describes the desired order. TDA provides some 
-`example order specs 
-<https://developer.tdameritrade.com/content/place-order-samples>`__ to
-illustrate the process and provides a schema in the `place order documentation 
-<https://developer.tdameritrade.com/account-access/apis/post/accounts/
-%7BaccountId%7D/orders-0>`__, but beyond that we're on our own.
-
-The ``tda.orders`` module provides an incomplete set of helpers for building 
-these order specs. The aim is to make it impossible to build an invalid JSON 
-object. For example, here is how you might use this module to place a market 
-order for ten shares of Apple common stock:
-
-.. code-block:: python
-
-  from tda.orders import EquityOrderBuilder, Duration, Session
-
-  builder = EquityOrderBuilder('AAPL', 10)
-  builder.set_instruction(EquityOrderBuilder.Instruction.SELL)
-  builder.set_order_type(EquityOrderBuilder.OrderType.MARKET)
-  builder.set_duration(Duration.DAY)
-  builder.set_session(Session.NORMAL)
-
-  client = ...  # Get a client however you see fit
-  account_id = 12345678
-
-  resp = client.place_order(account_id, builder.build())
-  assert resp.ok
-
--------------
-Common Values
--------------
-
-.. autoclass:: tda.orders.Duration
-  :members:
-  :undoc-members:
-.. autoclass:: tda.orders.Session
-  :members:
-  :undoc-members:
-
-.. autoexception:: tda.orders.InvalidOrderException
-
--------------
-Equity Orders
--------------
-
-.. autoclass:: tda.orders.EquityOrderBuilder
-  :members:
-  :undoc-members:
-
-  .. automethod:: __init__
diff --git a/tda/debug.py b/tda/debug.py
index f9be4ea..11dfcdc 100644
--- a/tda/debug.py
+++ b/tda/debug.py
@@ -7,6 +7,7 @@ import tda
 def get_logger():
     return logging.getLogger(__name__)
 
+
 class LogRedactor:
     '''
     Collects strings that should not be emitted and replaces them with safe
@@ -95,9 +96,10 @@ def enable_bug_report_logging():
     '''
     _enable_bug_report_logging()
 
+
 def _enable_bug_report_logging(output=sys.stderr, loggers=None):
     '''
-    Module-internal version of :func:`enable_bug_report_logging`, intended for 
+    Module-internal version of :func:`enable_bug_report_logging`, intended for
     use in tests.
     '''
     if loggers is None:
diff --git a/tda/orders.py b/tda/orders.py
deleted file mode 100644
index 6f74b90..0000000
--- a/tda/orders.py
+++ /dev/null
@@ -1,116 +0,0 @@
-from enum import Enum
-
-
-class InvalidOrderException(Exception):
-    '''Raised when attempting to build an incomplete order'''
-    pass
-
-
-class Duration(Enum):
-    DAY = 'DAY'
-    GOOD_TILL_CANCEL = 'GOOD_TILL_CANCEL'
-    FILL_OR_KILL = 'FILL_OR_KILL'
-
-
-class Session(Enum):
-    NORMAL = 'NORMAL'
-    AM = 'AM'
-    PM = 'PM'
-    SEAMESS = 'SEAMLESS'
-
-
-class EquityOrderBuilder:
-    '''Helper class to construct equity orders.'''
-
-    def __init__(self, symbol, quantity):
-        '''Create an order for the given symbol and quantity. Note all
-        unspecified parameters must be set prior to building the order spec.
-
-        :param symbol: Symbol for the order
-        :param quantity: Quantity of the order
-        '''
-        self.symbol = symbol
-        self.quantity = quantity
-
-        self.instruction = None
-        self.order_type = None
-        self.price = None
-        self.duration = None
-        self.session = None
-
-    def __assert_set(self, name):
-        value = getattr(self, name)
-        if value is None:
-            raise InvalidOrderException('{} must be set'.format(name))
-        return value
-
-    # Instructions
-    class Instruction(Enum):
-        '''Order instruction'''
-        BUY = 'BUY'
-        SELL = 'SELL'
-
-    def set_instruction(self, instruction):
-        '''Set the order instruction'''
-        assert isinstance(instruction, self.Instruction)
-        self.instruction = instruction
-        return self
-
-    # Order types
-    class OrderType(Enum):
-        '''Order type'''
-        MARKET = 'MARKET'
-        LIMIT = 'LIMIT'
-
-    def set_order_type(self, order_type):
-        '''Set the order type'''
-        assert isinstance(order_type, self.OrderType)
-        self.order_type = order_type
-        return self
-
-    # Price
-    def set_price(self, price):
-        '''Set the order price. Must be set for ``LIMIT`` orders.'''
-        assert price > 0.0
-        self.price = price
-        return self
-
-    # Durations
-    def set_duration(self, duration):
-        '''Set the order duration'''
-        assert isinstance(duration, Duration)
-        self.duration = duration
-        return self
-
-    # Sessions
-    def set_session(self, session):
-        '''Set the order's session'''
-        assert isinstance(session, Session)
-        self.session = session
-        return self
-
-    def build(self):
-        '''Build the order spec.
-
-        :raise InvalidOrderException: if the order is not fully specified
-        '''
-        spec = {
-            'orderType': self.__assert_set('order_type').value,
-            'session': self.__assert_set('session').value,
-            'duration': self.__assert_set('duration').value,
-            'orderStrategyType': 'SINGLE',
-            'orderLegCollection': [{
-                'instruction': self.__assert_set('instruction').value,
-                'quantity': self.quantity,
-                'instrument': {
-                    'symbol': self.symbol,
-                    'assetType': 'EQUITY'}
-            }]
-        }
-
-        if self.order_type == self.OrderType.LIMIT:
-            spec['price'] = self.__assert_set('price')
-        else:
-            assert self.price is None
-
-        return spec
diff --git a/tda/orders/__init__.py b/tda/orders/__init__.py
new file mode 100644
index 0000000..d295570
--- /dev/null
+++ b/tda/orders/__init__.py
@@ -0,0 +1,32 @@
+from enum import Enum
+
+from . import common
+from . import equities
+from . import generic
+
+__WARNINGS = set()
+
+
+def __getattr__(name):
+    import sys
+
+    def warn(new_package):
+        if name not in __WARNINGS:
+            print(('WARNING: {name} has moved from tda.orders to ' +
+                   'tda.orders.{new_package}. It will be removed from tda.orders ' +
+                   'in a future version.').format(
+                       name=name, new_package=new_package), file=sys.stderr)
+            __WARNINGS.add(name)
+
+    if name == 'InvalidOrderException':
+        warn('equities')
+        return common.InvalidOrderException
+    elif name == 'Duration':
+        warn('common')
+        return common.Duration
+    elif name == 'Session':
+        warn('common')
+        return common.Session
+    elif name == 'EquityOrderBuilder':
+        warn('equities')
+        return equities.EquityOrderBuilder
diff --git a/tda/orders/common.py b/tda/orders/common.py
new file mode 100644
index 0000000..6919a82
--- /dev/null
+++ b/tda/orders/common.py
@@ -0,0 +1,351 @@
+from enum import Enum
+
+
+class __BaseInstrument:
+    def __init__(self, asset_type, symbol):
+        self._assetType = asset_type
+        self._symbol = symbol
+
+
+class EquityInstrument(__BaseInstrument):
+    '''Represents an equity when creating order legs.'''
+
+    def __init__(self, symbol):
+        super().__init__('EQUITY', symbol)
+
+
+class OptionInstrument(__BaseInstrument):
+    '''Represents an option when creating order legs.'''
+
+    def __init__(self, symbol):
+        super().__init__('OPTION', symbol)
+
+
+class InvalidOrderException(Exception):
+    '''Raised when attempting to build an incomplete order'''
+    pass
+
+
+class Duration(Enum):
+    '''
+    Length of time over which the trade will be active.
+    '''
+    #: Cancel the trade at the end of the trading day. Note if the order cannot
+    #: be filled all at once, you may see partial executions throughout the day.
+    DAY = 'DAY'
+
+    # TODO: Implement order cancellation date
+
+    #: Keep the trade open for six months, or until the end of the cancel date,
+    #: whichever is shorter. Note if the order cannot be filled all at once, you
+    #: may see partial executions over the lifetime of the order.
+    GOOD_TILL_CANCEL = 'GOOD_TILL_CANCEL'
+
+    #: Either execute the order immediately at the specified price, or cancel it
+    #: immediately.
+    FILL_OR_KILL = 'FILL_OR_KILL'
+
+
+class Session(Enum):
+    '''
+    The market session during which the order trade should be executed.
+    '''
+
+    #: Normal market hours, from 9:30am to 4:00pm Eastern.
+    NORMAL = 'NORMAL'
+
+    #: Premarket session, from 8:00am to 9:30am Eastern.
+    AM = 'AM'
+
+    #: After-market session, from 4:00pm to 8:00pm Eastern.
+    PM = 'PM'
+
+    #: Unclear what this means. If you know, please `tell us
+    #: <https://github.com/alexgolec/tda-api/issues>`__.
+    SEAMESS = 'SEAMLESS'
+
+
+class OrderType(Enum):
+    '''
+    Type of equity or option order to place.
+    '''
+
+    #: Execute the order immediately at the best-available price.
+    #: `More Info <https://www.investopedia.com/terms/m/marketorder.asp>`__.
+    MARKET = 'MARKET'
+
+    #: Execute the order at your price or better. 
+    #: `More info <https://www.investopedia.com/terms/l/limitorder.asp>`__.
+    LIMIT = 'LIMIT'
+
+    #: Wait until the price reaches the stop price, and then immediately place a
+    #: market order.
+    #: `More Info <https://www.investopedia.com/terms/l/limitorder.asp>`__.
+    STOP = 'STOP'
+
+    #: Wait until the price reaches the stop price, and then immediately place a
+    #: limit order at the specified price. `More Info
+    #: <https://www.investopedia.com/terms/s/stop-limitorder.asp>`__.
+    STOP_LIMIT = 'STOP_LIMIT'
+
+    #: Similar to ``STOP``, except if the price moves in your favor, the stop
+    #: price is adjusted in that direction. Places a market order if the stop
+    #: condition is met.
+    #: `More info <https://www.investopedia.com/terms/t/trailingstop.asp>`__.
+    TRAILING_STOP = 'TRAILING_STOP'
+
+    #: Similar to ``STOP_LIMIT``, except if the price moves in your favor, the
+    #: stop price is adjusted in that direction. Places a limit order at the
+    #: specified price if the stop condition is met.
+    #: `More info <https://www.investopedia.com/terms/t/trailingstop.asp>`__.
+    TRAILING_STOP_LIMIT = 'TRAILING_STOP_LIMIT'
+
+    #: Place the order at the closing price immediately upon market close.
+    #: `More info <https://www.investopedia.com/terms/m/marketonclose.asp>`__
+    MARKET_ON_CLOSE = 'MARKET_ON_CLOSE'
+
+    #: Exercise an option.
+    EXERCISE = 'EXERCISE'
+
+    #: Place an order for an options spread resulting in a net debit.
+    #: `More info <https://www.investopedia.com/ask/answers/042215/
+    #: whats-difference-between-credit-spread-and-debt-spread.asp>`__
+    NET_DEBIT = 'NET_DEBIT'
+
+    #: Place an order for an options spread resulting in a net credit.
+    #: `More info <https://www.investopedia.com/ask/answers/042215/
+    #: whats-difference-between-credit-spread-and-debt-spread.asp>`__
+    NET_CREDIT = 'NET_CREDIT'
+
+    #: Place an order for an options spread resulting in neither a credit nor a
+    #: debit.
+    #: `More info <https://www.investopedia.com/ask/answers/042215/
+    #: whats-difference-between-credit-spread-and-debt-spread.asp>`__
+    NET_ZERO = 'NET_ZERO'
+
+
+class ComplexOrderStrategyType(Enum):
+    '''
+    Explicit order strategies for executing multi-leg options orders.
+    '''
+
+    #: No complex order strategy. This is the default.
+    NONE = 'NONE'
+
+    #: `Covered call <https://tickertape.tdameritrade.com/trading/
+    #: selling-covered-call-options-strategy-income-hedging-15135>`__
+    COVERED = 'COVERED'
+
+    #: `Vertical spread <https://tickertape.tdameritrade.com/trading/
+    #: vertical-credit-spreads-high-probability-15846>`__
+    VERTICAL = 'VERTICAL'
+
+    #: `Ratio backspread <https://tickertape.tdameritrade.com/trading/
+    #: pricey-stocks-ratio-spreads-15306>`__
+    BACK_RATIO = 'BACK_RATIO'
+
+    #: `Calendar spread <https://tickertape.tdameritrade.com/trading/
+    #: calendar-spreads-trading-primer-15095>`__
+    CALENDAR = 'CALENDAR'
+
+    #: `Diagonal spread <https://tickertape.tdameritrade.com/trading/
+    #: love-your-diagonal-spread-15030>`__
+    DIAGONAL = 'DIAGONAL'
+
+    #: `Straddle spread <https://tickertape.tdameritrade.com/trading/
+    #: straddle-strangle-option-volatility-16208>`__
+    STRADDLE = 'STRADDLE'
+
+    #: `Strandle spread <https://tickertape.tdameritrade.com/trading/
+    #: straddle-strangle-option-volatility-16208>`__
+    STRANGLE = 'STRANGLE'
+
+    COLLAR_SYNTHETIC = 'COLLAR_SYNTHETIC'
+
+    #: `Butterfly spread <https://tickertape.tdameritrade.com/trading/
+    #: butterfly-spread-options-15976>`__
+    BUTTERFLY = 'BUTTERFLY'
+
+    #: `Condor spread <https://www.investopedia.com/terms/c/
+    #: condorspread.asp>`__
+    CONDOR = 'CONDOR'
+
+    #: `Iron condor spread <https://tickertape.tdameritrade.com/trading/
+    #: iron-condor-options-spread-your-trading-wings-15948>`__
+    IRON_CONDOR = 'IRON_CONDOR'
+
+    #: `Roll a vertical spread <https://tickertape.tdameritrade.com/trading/
+    #: exit-winning-losing-trades-16685>`__
+    VERTICAL_ROLL = 'VERTICAL_ROLL'
+
+    #: `Collar strategy <https://tickertape.tdameritrade.com/trading/
+    #: stock-hedge-options-collars-15529>`__
+    COLLAR_WITH_STOCK = 'COLLAR_WITH_STOCK'
+
+    #: `Double diagonal spread <https://optionstradingiq.com/
+    #: the-ultimate-guide-to-double-diagonal-spreads/>`__
+    DOUBLE_DIAGONAL = 'DOUBLE_DIAGONAL'
+
+    #: `Unbalanced butterfy spread  <https://tickertape.tdameritrade.com/
+    #: trading/unbalanced-butterfly-strong-directional-bias-15913>`__
+    UNBALANCED_BUTTERFLY = 'UNBALANCED_BUTTERFLY'
+    UNBALANCED_CONDOR = 'UNBALANCED_CONDOR'
+    UNBALANCED_IRON_CONDOR = 'UNBALANCED_IRON_CONDOR'
+    UNBALANCED_VERTICAL_ROLL = 'UNBALANCED_VERTICAL_ROLL'
+
+    #: A custom multi-leg order strategy.
+    CUSTOM = 'CUSTOM'
+
+
+class Destination(Enum):
+    '''
+    Destinations for when you want to request a specific destination for your
+    order.
+    '''
+
+    INET = 'INET'
+    ECN_ARCA = 'ECN_ARCA'
+    CBOE = 'CBOE'
+    AMEX = 'AMEX'
+    PHLX = 'PHLX'
+    ISE = 'ISE'
+    BOX = 'BOX'
+    NYSE = 'NYSE'
+    NASDAQ = 'NASDAQ'
+    BATS = 'BATS'
+    C2 = 'C2'
+    AUTO = 'AUTO'
+
+
+class StopPriceLinkBasis(Enum):
+    MANUAL = 'MANUAL'
+    BASE = 'BASE'
+    TRIGGER = 'TRIGGER'
+    LAST = 'LAST'
+    BID = 'BID'
+    ASK = 'ASK'
+    ASK_BID = 'ASK_BID'
+    MARK = 'MARK'
+    AVERAGE = 'AVERAGE'
+
+
+class StopPriceLinkType(Enum):
+    VALUE = 'VALUE'
+    PERCENT = 'PERCENT'
+    TICK = 'TICK'
+
+
+class StopType(Enum):
+    STANDARD = 'STANDARD'
+    BID = 'BID'
+    ASK = 'ASK'
+    LAST = 'LAST'
+    MARK = 'MARK'
+
+
+class PriceLinkBasis(Enum):
+    MANUAL = 'MANUAL'
+    BASE = 'BASE'
+    TRIGGER = 'TRIGGER'
+    LAST = 'LAST'
+    BID = 'BID'
+    ASK = 'ASK'
+    ASK_BID = 'ASK_BID'
+    MARK = 'MARK'
+    AVERAGE = 'AVERAGE'
+
+
+class PriceLinkType(Enum):
+    VALUE = 'VALUE'
+    PERCENT = 'PERCENT'
+    TICK = 'TICK'
+
+
+class EquityInstruction(Enum):
+    '''
+    Instructions for opening and closing equity positions.
+    '''
+
+    #: Open a long equity position
+    BUY = 'BUY'
+
+    #: Close a long equity position
+    SELL = 'SELL'
+
+    #: Open a short equity position
+    SELL_SHORT = 'SELL_SHORT'
+
+    #: Close a short equity position
+    BUY_TO_COVER = 'BUY_TO_COVER'
+
+
+class OptionInstruction(Enum):
+    '''
+    Instructions for opening and closing options positions.
+    '''
+    #: Enter a new long option position
+    BUY_TO_OPEN = 'BUY_TO_OPEN'
+
+    #: Exit an existing long option position
+    SELL_TO_CLOSE = 'SELL_TO_CLOSE'
+
+    #: Enter a short position in an option
+    SELL_TO_OPEN = 'SELL_TO_OPEN'
+
+    #: Exit an existing short position in an option
+    BUY_TO_CLOSE = 'BUY_TO_CLOSE'
+
+
+class SpecialInstruction(Enum):
+    '''
+    Special instruction for trades.
+    '''
+
+    #: Disallow partial order execution.
+    #: `More info <https://www.investopedia.com/terms/a/aon.asp>`__.
+    ALL_OR_NONE = 'ALL_OR_NONE'
+
+    #: Do not reduce order size in response to cash dividends.
+    #: `More info <https://www.investopedia.com/terms/d/dnr.asp>`__.
+    DO_NOT_REDUCE = 'DO_NOT_REDUCE'
+
+    #: Combination of ``ALL_OR_NONE`` and ``DO_NOT_REDUCE``.
+    ALL_OR_NONE_DO_NOT_REDUCE = 'ALL_OR_NONE_DO_NOT_REDUCE'
+
+
+class OrderStrategyType(Enum):
+    '''
+    Rules for composite orders.
+    '''
+
+    #: No chaining, only a single order is submitted
+    SINGLE = 'SINGLE'
+
+    #: Execution of one order cancels the other
+    OCO = 'OCO'
+
+    #: Execution of one order triggers placement of the other
+    TRIGGER = 'TRIGGER'
+
+
+def one_cancels_other(order1, order2):
+    '''
+    If one of the orders is executed, immediately cancel the other.
+    '''
+    from tda.orders.generic import OrderBuilder
+
+    return (OrderBuilder()
+            .set_order_strategy_type(OrderStrategyType.OCO)
+            .add_child_order_strategy(order1)
+            .add_child_order_strategy(order2))
+
+
+def first_triggers_second(first_order, second_order):
+    '''
+    If ``first_order`` is executed, immediately place ``second_order``.
+    '''
+    from tda.orders.generic import OrderBuilder
+
+    return (first_order
+            .set_order_strategy_type(OrderStrategyType.TRIGGER)
+            .add_child_order_strategy(second_order))
diff --git a/tda/orders/equities.py b/tda/orders/equities.py
new file mode 100644
index 0000000..f6ae32f
--- /dev/null
+++ b/tda/orders/equities.py
@@ -0,0 +1,269 @@
+from enum import Enum
+
+from tda.orders.common import InvalidOrderException
+from tda.orders.common import InvalidOrderException, Duration, Session
+
+
+_DEPRECATION_WARNED = False
+
+
+class EquityOrderBuilder:
+    '''Helper class to construct equity orders.'''
+
+    def __init__(self, symbol, quantity):
+        '''Create an order for the given symbol and quantity. Note all
+        unspecified parameters must be set prior to building the order spec.
+
+        **WARNING:** This class is deprecated in favor of the
+        :ref:`tda.orders.generic.OrderBuilder` and the order template helpers.
+        It will be removed in a future release.
+
+        :param symbol: Symbol for the order
+        :param quantity: Quantity of the order
+        '''
+        global _DEPRECATION_WARNED
+        if not _DEPRECATION_WARNED:
+            import sys
+            print('WARNING: EquityOrderBuilder has been deprecated. Please ' +
+                  'migrate to one of the new order templates or use the ' +
+                  'generic OrderBuilder class.', file=sys.stderr)
+            _DEPRECATION_WARNED = True
+
+        self.symbol = symbol
+        self.quantity = quantity
+
+        self.instruction = None
+        self.order_type = None
+        self.price = None
+        self.duration = None
+        self.session = None
+
+    def __assert_set(self, name):
+        value = getattr(self, name)
+        if value is None:
+            raise InvalidOrderException('{} must be set'.format(name))
+        return value
+
+    # Instructions
+    class Instruction(Enum):
+        '''Order instruction'''
+        BUY = 'BUY'
+        SELL = 'SELL'
+
+    def set_instruction(self, instruction):
+        '''Set the order instruction'''
+        assert isinstance(instruction, self.Instruction)
+        self.instruction = instruction
+        return self
+
+    # Order types
+    class OrderType(Enum):
+        '''Order type'''
+        MARKET = 'MARKET'
+        LIMIT = 'LIMIT'
+
+    def set_order_type(self, order_type):
+        '''Set the order type'''
+        assert isinstance(order_type, self.OrderType)
+        self.order_type = order_type
+        return self
+
+    # Price
+    def set_price(self, price):
+        '''Set the order price. Must be set for ``LIMIT`` orders.'''
+        assert price > 0.0
+        self.price = price
+        return self
+
+    # Durations
+    def set_duration(self, duration):
+        '''Set the order duration'''
+        assert isinstance(duration, Duration)
+        self.duration = duration
+        return self
+
+    # Sessions
+    def set_session(self, session):
+        '''Set the order's session'''
+        assert isinstance(session, Session)
+        self.session = session
+        return self
+
+    def build(self):
+        '''Build the order spec.
+
+        :raise InvalidOrderException: if the order is not fully specified
+        '''
+        spec = {
+            'orderType': self.__assert_set('order_type').value,
+            'session': self.__assert_set('session').value,
+            'duration': self.__assert_set('duration').value,
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [{
+                'instruction': self.__assert_set('instruction').value,
+                'quantity': self.quantity,
+                'instrument': {
+                    'symbol': self.symbol,
+                    'assetType': 'EQUITY'}
+            }]
+        }
+
+        if self.order_type == self.OrderType.LIMIT:
+            spec['price'] = self.__assert_set('price')
+        else:
+            assert self.price is None
+
+        return spec
+
+
+##########################################################################
+# Buy orders
+
+
+def equity_buy_market(symbol, quantity):
+    '''
+    Returns a pre-filled :class:`~tda.orders.generic.OrderBuilder` for an equity
+    buy market order.
+    '''
+    from tda.orders.common import Duration, EquityInstruction
+    from tda.orders.common import OrderStrategyType, OrderType, Session
+    from tda.orders.generic import OrderBuilder
+
+    return (OrderBuilder()
+            .set_order_type(OrderType.MARKET)
+            .set_session(Session.NORMAL)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_equity_leg(EquityInstruction.BUY, symbol, quantity))
+
+
+def equity_buy_limit(symbol, quantity, price):
+    '''
+    Returns a pre-filled :class:`~tda.orders.generic.OrderBuilder` for an equity
+    buy limit order.
+    '''
+    from tda.orders.common import Duration, EquityInstruction
+    from tda.orders.common import OrderStrategyType, OrderType, Session
+    from tda.orders.generic import OrderBuilder
+
+    return (OrderBuilder()
+            .set_order_type(OrderType.LIMIT)
+            .set_price(price)
+            .set_session(Session.NORMAL)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_equity_leg(EquityInstruction.BUY, symbol, quantity))
+
+##########################################################################
+# Sell orders
+
+
+def equity_sell_market(symbol, quantity):
+    '''
+    Returns a pre-filled :class:`~tda.orders.generic.OrderBuilder` for an equity
+    sell market order.
+    '''
+    from tda.orders.common import Duration, EquityInstruction
+    from tda.orders.common import OrderStrategyType, OrderType, Session
+    from tda.orders.generic import OrderBuilder
+
+    return (OrderBuilder()
+            .set_order_type(OrderType.MARKET)
+            .set_session(Session.NORMAL)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_equity_leg(EquityInstruction.SELL, symbol, quantity))
+
+
+def equity_sell_limit(symbol, quantity, price):
+    '''
+    Returns a pre-filled :class:`~tda.orders.generic.OrderBuilder` for an equity
+    sell limit order.
+    '''
+    from tda.orders.common import Duration, EquityInstruction
+    from tda.orders.common import OrderStrategyType, OrderType, Session
+    from tda.orders.generic import OrderBuilder
+
+    return (OrderBuilder()
+            .set_order_type(OrderType.LIMIT)
+            .set_price(price)
+            .set_session(Session.NORMAL)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_equity_leg(EquityInstruction.SELL, symbol, quantity))
+
+##########################################################################
+# Short sell orders
+
+
+def equity_sell_short_market(symbol, quantity):
+    '''
+    Returns a pre-filled :class:`~tda.orders.generic.OrderBuilder` for an equity
+    short sell market order.
+    '''
+    from tda.orders.common import Duration, EquityInstruction
+    from tda.orders.common import OrderStrategyType, OrderType, Session
+    from tda.orders.generic import OrderBuilder
+
+    return (OrderBuilder()
+            .set_order_type(OrderType.MARKET)
+            .set_session(Session.NORMAL)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_equity_leg(EquityInstruction.SELL_SHORT, symbol, quantity))
+
+
+def equity_sell_short_limit(symbol, quantity, price):
+    '''
+    Returns a pre-filled :class:`~tda.orders.generic.OrderBuilder` for an equity
+    short sell limit order.
+    '''
+    from tda.orders.common import Duration, EquityInstruction
+    from tda.orders.common import OrderStrategyType, OrderType, Session
+    from tda.orders.generic import OrderBuilder
+
+    return (OrderBuilder()
+            .set_order_type(OrderType.LIMIT)
+            .set_price(price)
+            .set_session(Session.NORMAL)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_equity_leg(EquityInstruction.SELL_SHORT, symbol, quantity))
+
+##########################################################################
+# Buy to cover orders
+
+
+def equity_buy_to_cover_market(symbol, quantity):
+    '''
+    Returns a pre-filled :class:`~tda.orders.generic.OrderBuilder` for an equity
+    buy-to-cover market order.
+    '''
+    from tda.orders.common import Duration, EquityInstruction
+    from tda.orders.common import OrderStrategyType, OrderType, Session
+    from tda.orders.generic import OrderBuilder
+
+    return (OrderBuilder()
+            .set_order_type(OrderType.MARKET)
+            .set_session(Session.NORMAL)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_equity_leg(EquityInstruction.BUY_TO_COVER, symbol, quantity))
+
+
+def equity_buy_to_cover_limit(symbol, quantity, price):
+    '''
+    Returns a pre-filled :class:`~tda.orders.generic.OrderBuilder` for an equity
+    buy-to-cover limit order.
+    '''
+    from tda.orders.common import Duration, EquityInstruction
+    from tda.orders.common import OrderStrategyType, OrderType, Session
+    from tda.orders.generic import OrderBuilder
+
+    return (OrderBuilder()
+            .set_order_type(OrderType.LIMIT)
+            .set_price(price)
+            .set_session(Session.NORMAL)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_equity_leg(EquityInstruction.BUY_TO_COVER, symbol, quantity))
diff --git a/tda/orders/generic.py b/tda/orders/generic.py
new file mode 100644
index 0000000..afe2be1
--- /dev/null
+++ b/tda/orders/generic.py
@@ -0,0 +1,434 @@
+from enum import Enum
+
+from tda.orders import common
+from tda.utils import EnumEnforcer
+
+
+def _build_object(obj):
+    # Literals are passed straight through
+    if isinstance(obj, str) or isinstance(obj, int) or isinstance(obj, float):
+        return obj
+
+    # Note enums are not handled because call callers convert their enums to
+    # values.
+
+    # Dicts and lists are iterated over, with keys intact
+    elif isinstance(obj, dict):
+        return dict((key, _build_object(value)) for key, value in obj.items())
+    elif isinstance(obj, list):
+        return [_build_object(i) for i in obj]
+
+    # Objects have their variables translated into keys
+    else:
+        ret = {}
+        for name, value in vars(obj).items():
+            if value is None or name[0] != '_':
+                continue
+
+            name = name[1:]
+            ret[name] = _build_object(value)
+        return ret
+
+
+def _truncate_float(flt):
+    return '{:.2f}'.format(float(int(flt * 100)) / 100.0)
+
+
+class OrderBuilder(EnumEnforcer):
+    '''
+    Helper class to create arbitrarily complex orders. Note this class simply
+    implements the order schema defined in the `documentation
+    <https://developer.tdameritrade.com/account-access/apis/post/accounts/
+    %7BaccountId%7D/orders-0>`__, with no attempts to validate the result.
+    Orders created using this class may be rejected or may never fill. Use at
+    your own risk.
+    '''
+
+    def __init__(self, *, enforce_enums=True):
+        super().__init__(enforce_enums)
+
+        self._session = None
+        self._duration = None
+        self._orderType = None
+        self._complexOrderStrategyType = None
+        self._quantity = None
+        self._requestedDestination = None
+        self._stopPrice = None
+        self._stopPriceLinkBasis = None
+        self._stopPriceLinkType = None
+        self._stopPriceOffset = None
+        self._stopType = None
+        self._priceLinkBasis = None
+        self._priceLinkType = None
+        self._price = None
+        self._orderLegCollection = None
+        self._activationPrice = None
+        self._specialInstruction = None
+        self._orderStrategyType = None
+        self._childOrderStrategies = None
+
+    # Session
+    def set_session(self, session):
+        '''
+        Set the order session. See :class:`~tda.orders.common.Session` for
+        details.
+        '''
+        session = self.convert_enum(session, common.Session)
+        self._session = session
+        return self
+
+    def clear_session(self):
+        '''
+        Clear the order session.
+        '''
+        self._session = None
+        return self
+
+    # Duration
+    def set_duration(self, duration):
+        '''
+        Set the order duration. See :class:`~tda.orders.common.Duration` for
+        details.
+        '''
+        duration = self.convert_enum(duration, common.Duration)
+        self._duration = duration
+        return self
+
+    def clear_duration(self):
+        '''
+        Clear the order duration.
+        '''
+        self._duration = None
+        return self
+
+    # OrderType
+    def set_order_type(self, order_type):
+        '''
+        Set the order type. See :class:`~tda.orders.common.OrderType` for
+        details.
+        '''
+        order_type = self.convert_enum(order_type, common.OrderType)
+        self._orderType = order_type
+        return self
+
+    def clear_order_type(self):
+        '''
+        Clear the order type.
+        '''
+        self._orderType = None
+        return self
+
+    # ComplexOrderStrategyType
+    def set_complex_order_strategy_type(self, complex_order_strategy_type):
+        '''
+        Set the complex order strategy type. See
+        :class:`~tda.orders.common.ComplexOrderStrategyType` for details.
+        '''
+        complex_order_strategy_type = self.convert_enum(
+            complex_order_strategy_type, common.ComplexOrderStrategyType)
+        self._complexOrderStrategyType = complex_order_strategy_type
+        return self
+
+    def clear_complex_order_strategy_type(self):
+        '''
+        Clear the complex order strategy type.
+        '''
+        self._complexOrderStrategyType = None
+        return self
+
+    # Quantity
+    def set_quantity(self, quantity):
+        '''
+        Exact semantics unknown. See :ref:`undocumented_quantity` for a
+        discussion.
+        '''
+        if quantity <= 0:
+            raise ValueError('quantity must be positive')
+        self._quantity = quantity
+        return self
+
+    def clear_quantity(self):
+        '''
+        Clear the order-level quantity. Note this does not affect order legs.
+        '''
+        self._quantity = None
+        return self
+
+    # RequestedDestination
+    def set_requested_destination(self, requested_destination):
+        '''
+        Set the requested destination. See
+        :class:`~tda.orders.common.Destination` for details.
+        '''
+        requested_destination = self.convert_enum(
+            requested_destination, common.Destination)
+        self._requestedDestination = requested_destination
+        return self
+
+    def clear_requested_destination(self):
+        '''
+        Clear the requested destination.
+        '''
+        self._requestedDestination = None
+        return self
+
+    # StopPrice
+    def set_stop_price(self, stop_price):
+        '''
+        Set the stop price.
+        '''
+        self._stopPrice = _truncate_float(stop_price)
+        return self
+
+    def clear_stop_price(self):
+        '''
+        Clear the stop price.
+        '''
+        self._stopPrice = None
+        return self
+
+    # StopPriceLinkBasis
+    def set_stop_price_link_basis(self, stop_price_link_basis):
+        '''
+        Set the stop price link basis. See
+        :class:`~tda.orders.common.StopPriceLinkBasis` for details.
+        '''
+        stop_price_link_basis = self.convert_enum(
+            stop_price_link_basis, common.StopPriceLinkBasis)
+        self._stopPriceLinkBasis = stop_price_link_basis
+        return self
+
+    def clear_stop_price_link_basis(self):
+        '''
+        Clear the stop price link basis.
+        '''
+        self._stopPriceLinkBasis = None
+        return self
+
+    # StopPriceLinkType
+    def set_stop_price_link_type(self, stop_price_link_type):
+        '''
+        Set the stop price link type. See
+        :class:`~tda.orders.common.StopPriceLinkType` for details.
+        '''
+        stop_price_link_type = self.convert_enum(
+            stop_price_link_type, common.StopPriceLinkType)
+        self._stopPriceLinkType = stop_price_link_type
+        return self
+
+    def clear_stop_price_link_type(self):
+        '''
+        Clear the stop price link type.
+        '''
+        self._stopPriceLinkType = None
+        return self
+
+    # StopPriceOffset
+    def set_stop_price_offset(self, stop_price_offset):
+        '''
+        Set the stop price offset.
+        '''
+        self._stopPriceOffset = stop_price_offset
+        return self
+
+    def clear_stop_price_offset(self):
+        '''
+        Clear the stop price offset.
+        '''
+        self._stopPriceOffset = None
+        return self
+
+    # StopType
+    def set_stop_type(self, stop_type):
+        '''
+        Set the stop type. See
+        :class:`~tda.orders.common.StopType` for more details.
+        '''
+        stop_type = self.convert_enum(stop_type, common.StopType)
+        self._stopType = stop_type
+        return self
+
+    def clear_stop_type(self):
+        '''
+        Clear the stop type.
+        '''
+        self._stopType = None
+        return self
+
+    # PriceLinkBasis
+    def set_price_link_basis(self, price_link_basis):
+        '''
+        Set the price link basis. See
+        :class:`~tda.orders.common.PriceLinkBasis` for details.
+        '''
+        price_link_basis = self.convert_enum(
+            price_link_basis, common.PriceLinkBasis)
+        self._priceLinkBasis = price_link_basis
+        return self
+
+    def clear_price_link_basis(self):
+        '''
+        Clear the price link basis.
+        '''
+        self._priceLinkBasis = None
+        return self
+
+    # PriceLinkType
+    def set_price_link_type(self, price_link_type):
+        '''
+        Set the price link type. See
+        :class:`~tda.orders.common.PriceLinkType` for more details.
+        '''
+        price_link_type = self.convert_enum(
+            price_link_type, common.PriceLinkType)
+        self._priceLinkType = price_link_type
+        return self
+
+    def clear_price_link_type(self):
+        '''
+        Clear the price link basis.
+        '''
+        self._priceLinkType = None
+        return self
+
+    # Price
+    def set_price(self, price):
+        '''
+        Set the order price.
+        '''
+        self._price = _truncate_float(price)
+        return self
+
+    def clear_price(self):
+        '''
+        Clear the order price
+        '''
+        self._price = None
+        return self
+
+    # ActivationPrice
+    def set_activation_price(self, activation_price):
+        '''
+        Set the activation price.
+        '''
+        if activation_price <= 0.0:
+            raise ValueError('activation price must be positive')
+        self._activationPrice = activation_price
+        return self
+
+    def clear_activation_price(self):
+        '''
+        Clear the activation price.
+        '''
+        self._activationPrice = None
+        return self
+
+    # SpecialInstruction
+    def set_special_instruction(self, special_instruction):
+        '''
+        Set the special instruction. See
+        :class:`~tda.orders.common.SpecialInstruction` for details.
+        '''
+        special_instruction = self.convert_enum(
+            special_instruction, common.SpecialInstruction)
+        self._specialInstruction = special_instruction
+        return self
+
+    def clear_special_instruction(self):
+        '''
+        Clear the special instruction.
+        '''
+        self._specialInstruction = None
+        return self
+
+    # OrderStrategyType
+    def set_order_strategy_type(self, order_strategy_type):
+        '''
+        Set the order strategy type. See
+        :class:`~tda.orders.common.OrderStrategyType` for more details.
+        '''
+        order_strategy_type = self.convert_enum(
+            order_strategy_type, common.OrderStrategyType)
+        self._orderStrategyType = order_strategy_type
+        return self
+
+    def clear_order_strategy_type(self):
+        '''
+        Clear the order strategy type.
+        '''
+        self._orderStrategyType = None
+        return self
+
+    # ChildOrderStrategies
+    def add_child_order_strategy(self, child_order_strategy):
+        if (not isinstance(child_order_strategy, OrderBuilder)
+                and not isinstance(child_order_strategy, dict)):
+            raise ValueError('child order must be OrderBuilder or dict')
+
+        if self._childOrderStrategies is None:
+            self._childOrderStrategies = []
+
+        self._childOrderStrategies.append(child_order_strategy)
+        return self
+
+    def clear_child_order_strategies(self):
+        self._childOrderStrategies = None
+        return self
+
+    # OrderLegCollection
+    def __add_order_leg(self, instruction, instrument, quantity):
+        # instruction is assumed to have been verified
+
+        if quantity <= 0:
+            raise ValueError('quantity must be positive')
+
+        if self._orderLegCollection is None:
+            self._orderLegCollection = []
+
+        self._orderLegCollection.append({
+            'instruction': instruction,
+            'instrument': instrument,
+            'quantity': quantity,
+        })
+
+        return self
+
+    def add_equity_leg(self, instruction, symbol, quantity):
+        '''
+        Add an equity order leg.
+
+        :param instruction: Instruction for the leg. See
+                            :class:`~tda.orders.common.EquityInstruction` for
+                            valid options.
+        :param symbol: Equity symbol
+        :param quantity: Number of shares for the order
+        '''
+        instruction = self.convert_enum(instruction, common.EquityInstruction)
+        return self.__add_order_leg(
+            instruction, common.EquityInstrument(symbol), quantity)
+
+    def add_option_leg(self, instruction, symbol, quantity):
+        '''
+        Add an option order leg.
+
+        :param instruction: Instruction for the leg. See
+                            :class:`~tda.orders.common.OptionInstruction` for
+                            valid options.
+        :param symbol: Option symbol
+        :param quantity: Number of contracts for the order
+        '''
+        instruction = self.convert_enum(instruction, common.OptionInstruction)
+        return self.__add_order_leg(
+            instruction, common.OptionInstrument(symbol), quantity)
+
+    def clear_order_legs(self):
+        '''
+        Clear all order legs.
+        '''
+        self._orderLegCollection = None
+        return self
+
+    # Build
+
+    def build(self):
+        return _build_object(self)
diff --git a/tda/utils.py b/tda/utils.py
index 35b4bb5..152ae10 100644
--- a/tda/utils.py
+++ b/tda/utils.py
@@ -5,8 +5,6 @@ import datetime
 import dateutil.parser
 import re
 
-from .orders import EquityOrderBuilder
-
 
 class EnumEnforcer:
     def __init__(self, enforce_enums):


Test patch:
diff --git a/tests/orders/__init__.py b/tests/orders/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/orders/common_test.py b/tests/orders/common_test.py
new file mode 100644
index 0000000..3709c31
--- /dev/null
+++ b/tests/orders/common_test.py
@@ -0,0 +1,31 @@
+from tests.test_utils import has_diff, no_duplicates
+from tda.orders.common import *
+from tda.orders.generic import OrderBuilder
+
+import unittest
+
+class MultiOrderTest(unittest.TestCase):
+
+    def test_oco(self):
+        self.assertFalse(has_diff({
+            'orderStrategyType': 'OCO',
+            'childOrderStrategies': [
+                {'session': 'NORMAL'},
+                {'duration': 'DAY'},
+            ]
+        }, one_cancels_other(
+            OrderBuilder().set_session(Session.NORMAL),
+            OrderBuilder().set_duration(Duration.DAY)).build()))
+
+    def test_trigger(self):
+        self.assertFalse(has_diff({
+            'orderStrategyType': 'TRIGGER',
+            'session': 'NORMAL',
+            'childOrderStrategies': [
+                {'duration': 'DAY'},
+            ]
+        }, first_triggers_second(
+            OrderBuilder().set_session(Session.NORMAL),
+            OrderBuilder().set_duration(Duration.DAY)).build()))
+
+
diff --git a/tests/orders/generic_test.py b/tests/orders/generic_test.py
new file mode 100644
index 0000000..ca85cc5
--- /dev/null
+++ b/tests/orders/generic_test.py
@@ -0,0 +1,1066 @@
+import unittest
+
+from tda.orders.generic import *
+from tda.orders.common import *
+from tests.test_utils import has_diff, no_duplicates
+
+
+class OrderBuilderTest(unittest.TestCase):
+
+    def setUp(self):
+        self.maxDiff = None
+        self.order_builder = OrderBuilder()
+
+    ##########################################################################
+    # Session
+
+    @no_duplicates
+    def test_session_success(self):
+        self.order_builder.set_session(Session.NORMAL)
+        self.assertFalse(has_diff({
+            'session': 'NORMAL'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_session()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_session_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_session('NORMAL')
+
+    @no_duplicates
+    def test_session_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_session('NORMAL')
+        self.assertFalse(has_diff({
+            'session': 'NORMAL'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # Duration
+
+    @no_duplicates
+    def test_duration_success(self):
+        self.order_builder.set_duration(Duration.DAY)
+        self.assertFalse(has_diff({
+            'duration': 'DAY'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_duration()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_duration_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_duration('DAY')
+
+    @no_duplicates
+    def test_duration_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_duration('DAY')
+        self.assertFalse(has_diff({
+            'duration': 'DAY'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # OrderType
+
+    @no_duplicates
+    def test_order_type_success(self):
+        self.order_builder.set_order_type(OrderType.MARKET)
+        self.assertFalse(has_diff({
+            'orderType': 'MARKET'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_order_type()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_order_type_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_order_type('MARKET')
+
+    @no_duplicates
+    def test_order_type_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_order_type('MARKET')
+        self.assertFalse(has_diff({
+            'orderType': 'MARKET'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # ComplexOrderStrategyType
+
+    @no_duplicates
+    def test_complex_order_strategy_type_success(self):
+        self.order_builder.set_complex_order_strategy_type(
+            ComplexOrderStrategyType.IRON_CONDOR)
+        self.assertFalse(has_diff({
+            'complexOrderStrategyType': 'IRON_CONDOR'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_complex_order_strategy_type()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test__wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_complex_order_strategy_type('IRON_CONDOR')
+
+    @no_duplicates
+    def test__wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_complex_order_strategy_type('IRON_CONDOR')
+        self.assertFalse(has_diff({
+            'complexOrderStrategyType': 'IRON_CONDOR'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # Quantity
+
+    @no_duplicates
+    def test_quantity_success(self):
+        self.order_builder.set_quantity(12)
+        self.assertFalse(has_diff({
+            'quantity': 12
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_quantity()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_quantity_negative(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_quantity(-12)
+
+    @no_duplicates
+    def test_quantity_zero(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_quantity(0)
+
+    ##########################################################################
+    # RequestedDestination
+
+    @no_duplicates
+    def test_requested_destination_success(self):
+        self.order_builder.set_requested_destination(Destination.INET)
+        self.assertFalse(has_diff({
+            'requestedDestination': 'INET'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_requested_destination()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_requested_destination_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_requested_destination('INET')
+
+    @no_duplicates
+    def test_requested_destination_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_requested_destination('INET')
+        self.assertFalse(has_diff({
+            'requestedDestination': 'INET'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # StopPrice
+
+    @no_duplicates
+    def test_stop_price_success(self):
+        self.order_builder.set_stop_price(42.90)
+        self.assertFalse(has_diff({
+            'stopPrice': '42.90'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_stop_price()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_stop_price_negative(self):
+        self.order_builder.set_stop_price(-1.31)
+        self.assertFalse(has_diff({
+            'stopPrice': '-1.31'
+        }, self.order_builder.build()))
+
+    @no_duplicates
+    def test_stop_price_zero(self):
+        self.order_builder.set_stop_price(0)
+        self.assertFalse(has_diff({
+            'stopPrice': '0.00'
+        }, self.order_builder.build()))
+
+    @no_duplicates
+    def test_stop_price_do_not_round_up(self):
+        self.order_builder.set_stop_price(1.99999)
+        self.assertFalse(has_diff({
+            'stopPrice': '1.99'
+        }, self.order_builder.build()))
+
+    @no_duplicates
+    def test_stop_price_do_round_down(self):
+        self.order_builder.set_stop_price(2.00001)
+        self.assertFalse(has_diff({
+            'stopPrice': '2.00'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # StopPriceLinkBasis
+
+    @no_duplicates
+    def test_stop_price_link_basis_success(self):
+        self.order_builder.set_stop_price_link_basis(StopPriceLinkBasis.ASK)
+        self.assertFalse(has_diff({
+            'stopPriceLinkBasis': 'ASK'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_stop_price_link_basis()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_stop_price_link_basis_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_stop_price_link_basis('ASK')
+
+    @no_duplicates
+    def test_stop_price_link_basis_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_stop_price_link_basis('ASK')
+        self.assertFalse(has_diff({
+            'stopPriceLinkBasis': 'ASK'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # StopPriceLinkType
+
+    @no_duplicates
+    def test_stop_price_link_type_success(self):
+        self.order_builder.set_stop_price_link_type(StopPriceLinkType.VALUE)
+        self.assertFalse(has_diff({
+            'stopPriceLinkType': 'VALUE'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_stop_price_link_type()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_stop_price_link_type_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_stop_price_link_type('VALUE')
+
+    @no_duplicates
+    def test_stop_price_link_type_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_stop_price_link_type('VALUE')
+        self.assertFalse(has_diff({
+            'stopPriceLinkType': 'VALUE'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # StopPriceOffset
+
+    @no_duplicates
+    def test_stop_price_offset_success(self):
+        self.order_builder.set_stop_price_offset(12.98)
+        self.assertFalse(has_diff({
+            'stopPriceOffset': 12.98
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_stop_price_offset()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    ##########################################################################
+    # StopType
+
+    @no_duplicates
+    def test_stop_type_success(self):
+        self.order_builder.set_stop_type(StopType.MARK)
+        self.assertFalse(has_diff({
+            'stopType': 'MARK'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_stop_type()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_stop_type_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_stop_type('MARK')
+
+    @no_duplicates
+    def test_stop_type_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_stop_type('MARK')
+        self.assertFalse(has_diff({
+            'stopType': 'MARK'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # PriceLinkBasis
+
+    @no_duplicates
+    def test_price_link_basis_success(self):
+        self.order_builder.set_price_link_basis(PriceLinkBasis.AVERAGE)
+        self.assertFalse(has_diff({
+            'priceLinkBasis': 'AVERAGE'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_price_link_basis()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_price_link_basis_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_price_link_basis('AVERAGE')
+
+    @no_duplicates
+    def test_price_link_basis_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_price_link_basis('AVERAGE')
+        self.assertFalse(has_diff({
+            'priceLinkBasis': 'AVERAGE'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # PriceLinkType
+
+    @no_duplicates
+    def test_price_link_type_success(self):
+        self.order_builder.set_price_link_type(PriceLinkType.PERCENT)
+        self.assertFalse(has_diff({
+            'priceLinkType': 'PERCENT'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_price_link_type()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_price_link_type_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_price_link_type('PERCENT')
+
+    @no_duplicates
+    def test_price_link_type_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_price_link_type('PERCENT')
+        self.assertFalse(has_diff({
+            'priceLinkType': 'PERCENT'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # Price
+
+    @no_duplicates
+    def test_price_success(self):
+        self.order_builder.set_price(23.49)
+        self.assertFalse(has_diff({
+            'price': '23.49'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_price()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_price_negative(self):
+        self.order_builder.set_price(-1.23)
+        self.assertFalse(has_diff({
+            'price': '-1.23'
+        }, self.order_builder.build()))
+
+    @no_duplicates
+    def test_price_zero(self):
+        self.order_builder.set_price(0.0)
+        self.assertFalse(has_diff({
+            'price': '0.00'
+        }, self.order_builder.build()))
+
+    @no_duplicates
+    def test_price_do_not_round_up(self):
+        self.order_builder.set_price(19.9999999)
+        self.assertFalse(has_diff({
+            'price': '19.99'
+        }, self.order_builder.build()))
+
+    @no_duplicates
+    def test_price_do_not_round_down(self):
+        self.order_builder.set_price(20.00000001)
+        self.assertFalse(has_diff({
+            'price': '20.00'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # ActivationPrice
+
+    @no_duplicates
+    def test_activation_price_success(self):
+        self.order_builder.set_activation_price(54.03)
+        self.assertFalse(has_diff({
+            'activationPrice': 54.03
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_activation_price()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_activation_price_negative(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_activation_price(-3.94)
+
+    @no_duplicates
+    def test_activation_price_zero(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_activation_price(0.0)
+
+    ##########################################################################
+    # SpecialInstruction
+
+    @no_duplicates
+    def test_special_instruction_success(self):
+        self.order_builder.set_special_instruction(
+            SpecialInstruction.DO_NOT_REDUCE)
+        self.assertFalse(has_diff({
+            'specialInstruction': 'DO_NOT_REDUCE'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_special_instruction()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_special_instruction_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_special_instruction('DO_NOT_REDUCE')
+
+    @no_duplicates
+    def test_special_instruction_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_special_instruction('DO_NOT_REDUCE')
+        self.assertFalse(has_diff({
+            'specialInstruction': 'DO_NOT_REDUCE'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # OrderStrategyType
+
+    @no_duplicates
+    def test_order_strategy_type_success(self):
+        self.order_builder.set_order_strategy_type(OrderStrategyType.OCO)
+        self.assertFalse(has_diff({
+            'orderStrategyType': 'OCO'
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_order_strategy_type()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_order_strategy_type_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_order_strategy_type('OCO')
+
+    @no_duplicates
+    def test_order_strategy_type_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_order_strategy_type('OCO')
+        self.assertFalse(has_diff({
+            'orderStrategyType': 'OCO'
+        }, self.order_builder.build()))
+
+    ##########################################################################
+    # ChildOrderStrategies
+
+    @no_duplicates
+    def test_add_child_order_strategy_success(self):
+        self.order_builder.add_child_order_strategy(
+            OrderBuilder().set_session(Session.NORMAL))
+        self.assertFalse(has_diff({
+            'childOrderStrategies': [{'session': 'NORMAL'}]
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_child_order_strategies()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_add_child_order_strategy_dict(self):
+        self.order_builder.add_child_order_strategy(
+            {'session': 'NORMAL'})
+        self.assertFalse(has_diff({
+            'childOrderStrategies': [{'session': 'NORMAL'}]
+        }, self.order_builder.build()))
+
+    @no_duplicates
+    def test_add_child_order_strategy_invalid_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.add_child_order_strategy(10)
+
+    ##########################################################################
+    # OrderLegCollection
+
+    @no_duplicates
+    def test_add_equity_leg_success(self):
+        self.order_builder.add_equity_leg(EquityInstruction.BUY, 'GOOG', 10)
+        self.order_builder.add_equity_leg(
+            EquityInstruction.SELL_SHORT, 'MSFT', 1)
+        self.assertFalse(has_diff({
+            'orderLegCollection': [{
+                'instruction': 'BUY',
+                'instrument': {
+                    'symbol': 'GOOG',
+                    'assetType': 'EQUITY'
+                },
+                'quantity': 10,
+            }, {
+                'instruction': 'SELL_SHORT',
+                'instrument': {
+                    'symbol': 'MSFT',
+                    'assetType': 'EQUITY'
+                },
+                'quantity': 1,
+            }]
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_order_legs()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_add_equity_leg_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.add_equity_leg('BUY', 'GOOG', 10)
+
+    @no_duplicates
+    def test_add_equity_leg_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+
+        self.order_builder.add_equity_leg('BUY', 'GOOG', 10)
+        self.order_builder.add_equity_leg('SELL_TO_CLOSE', 'MSFT', 1)
+
+        self.assertFalse(has_diff({
+            'orderLegCollection': [{
+                'instruction': 'BUY',
+                'instrument': {
+                    'symbol': 'GOOG',
+                    'assetType': 'EQUITY'
+                },
+                'quantity': 10,
+            }, {
+                'instruction': 'SELL_TO_CLOSE',
+                'instrument': {
+                    'symbol': 'MSFT',
+                    'assetType': 'EQUITY'
+                },
+                'quantity': 1,
+            }]
+        }, self.order_builder.build()))
+
+    @no_duplicates
+    def test_add_equity_leg_negative_quantity(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.add_equity_leg(
+                EquityInstruction.BUY, 'GOOG', -1)
+
+    @no_duplicates
+    def test_add_equity_leg_zero_quantity(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.add_equity_leg(
+                EquityInstruction.BUY, 'GOOG', 0)
+
+    @no_duplicates
+    def test_add_option_leg_success(self):
+        self.order_builder.add_option_leg(
+            OptionInstruction.BUY_TO_OPEN, 'GOOG31433C1342', 10)
+        self.order_builder.add_option_leg(
+            OptionInstruction.BUY_TO_CLOSE, 'MSFT439132P35', 1)
+        self.assertFalse(has_diff({
+            'orderLegCollection': [{
+                'instruction': 'BUY_TO_OPEN',
+                'instrument': {
+                    'symbol': 'GOOG31433C1342',
+                    'assetType': 'OPTION'
+                },
+                'quantity': 10,
+            }, {
+                'instruction': 'BUY_TO_CLOSE',
+                'instrument': {
+                    'symbol': 'MSFT439132P35',
+                    'assetType': 'OPTION'
+                },
+                'quantity': 1,
+            }]
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_order_legs()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_add_option_leg_wrong_type(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.add_option_leg(
+                'BUY_TO_OPEN', 'GOOG31433C1342', 10)
+
+    @no_duplicates
+    def test_add_option_leg_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+
+        self.order_builder.add_option_leg('BUY_TO_OPEN', 'GOOG31433C1342', 10)
+        self.order_builder.add_option_leg('BUY_TO_CLOSE', 'MSFT439132P35', 1)
+        self.assertFalse(has_diff({
+            'orderLegCollection': [{
+                'instruction': 'BUY_TO_OPEN',
+                'instrument': {
+                    'symbol': 'GOOG31433C1342',
+                    'assetType': 'OPTION'
+                },
+                'quantity': 10,
+            }, {
+                'instruction': 'BUY_TO_CLOSE',
+                'instrument': {
+                    'symbol': 'MSFT439132P35',
+                    'assetType': 'OPTION'
+                },
+                'quantity': 1,
+            }]
+        }, self.order_builder.build()))
+
+        self.order_builder.clear_order_legs()
+        self.assertFalse(has_diff({}, self.order_builder.build()))
+
+    @no_duplicates
+    def test_add_option_leg_negative_quantity(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.add_option_leg(
+                OptionInstruction.BUY_TO_OPEN, 'GOOG31433C1342', -1)
+
+    @no_duplicates
+    def test_add_option_leg_zero_quantity(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.add_option_leg(
+                OptionInstruction.BUY_TO_OPEN, 'GOOG31433C1342', 0)
+
+
+class OrderBuilderExamplesTest(unittest.TestCase):
+
+    def setUp(self):
+        self.maxDiff = None
+
+    ##########################################################################
+    # Functional tests from here:
+    # https://developer.tdameritrade.com/content/place-order-samples
+    @no_duplicates
+    def test_quantity_negative(self):
+        with self.assertRaises(ValueError):
+            self.order_builder.set_quantity(-12)
+
+    @no_duplicates
+    def test_quantity_wrong_type_no_check(self):
+        self.order_builder = OrderBuilder(enforce_enums=False)
+        self.order_builder.set_quantity('')
+        self.assertFalse(has_diff({
+            'quantity': ''
+        }, self.order_builder.build()))
+
+
+class OrderBuilderExamplesTest(unittest.TestCase):
+
+    @no_duplicates
+    def setUp(self):
+        self.maxDiff = None
+
+    ##########################################################################
+    # Functional tests from here:
+    # https://developer.tdameritrade.com/content/place-order-samples
+
+    @no_duplicates
+    def test_buy_market_stock(self):
+        builder = (
+            OrderBuilder()
+            .set_order_type(OrderType.MARKET)
+            .set_session(Session.NORMAL)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_equity_leg(EquityInstruction.BUY, 'XYZ', 15))
+
+        expected = {
+            'orderType': 'MARKET',
+            'session': 'NORMAL',
+            'duration': 'DAY',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [
+                {
+                    'instruction': 'BUY',  # The original says 'Buy'
+                    'quantity': 15,
+                    'instrument': {
+                        'symbol': 'XYZ',
+                        'assetType': 'EQUITY'
+                    }
+                }
+            ]
+        }
+
+        self.assertFalse(has_diff(expected, builder.build()))
+
+    @no_duplicates
+    def test_buy_limit_single_option(self):
+        builder = (
+            OrderBuilder()
+            .set_complex_order_strategy_type(ComplexOrderStrategyType.NONE)
+            .set_order_type(OrderType.LIMIT)
+            .set_session(Session.NORMAL)
+            .set_price(6.45)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_option_leg(OptionInstruction.BUY_TO_OPEN, 'XYZ_032015C49', 10))
+
+        expected = {
+            'complexOrderStrategyType': 'NONE',
+            'orderType': 'LIMIT',
+            'session': 'NORMAL',
+            'price': '6.45',
+            'duration': 'DAY',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [
+                {
+                    'instruction': 'BUY_TO_OPEN',
+                    'quantity': 10,
+                    'instrument': {
+                        'symbol': 'XYZ_032015C49',
+                        'assetType': 'OPTION'
+                    }
+                }
+            ]
+        }
+
+        self.assertFalse(has_diff(expected, builder.build()))
+
+    @no_duplicates
+    def test_buy_limit_vertical_call_spread(self):
+        builder = (
+            OrderBuilder()
+            .set_order_type(OrderType.NET_DEBIT)
+            .set_session(Session.NORMAL)
+            .set_price(1.20)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_option_leg(OptionInstruction.BUY_TO_OPEN, 'XYZ_011516C40', 10)
+            .add_option_leg(
+                OptionInstruction.SELL_TO_OPEN, 'XYZ_011516C42.5', 10))
+
+        expected = {
+            'orderType': 'NET_DEBIT',
+            'session': 'NORMAL',
+            'price': '1.20',
+            'duration': 'DAY',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [
+                {
+                    'instruction': 'BUY_TO_OPEN',
+                    'quantity': 10,
+                    'instrument': {
+                        'symbol': 'XYZ_011516C40',
+                        'assetType': 'OPTION'
+                    }
+                },
+                {
+                    'instruction': 'SELL_TO_OPEN',
+                    'quantity': 10,
+                    'instrument': {
+                        'symbol': 'XYZ_011516C42.5',
+                        'assetType': 'OPTION'
+                    }
+                }
+            ]
+        }
+
+        self.assertFalse(has_diff(expected, builder.build()))
+
+    @no_duplicates
+    def test_custom_option_spread(self):
+        builder = (
+            OrderBuilder()
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .set_order_type(OrderType.MARKET)
+            .add_option_leg(OptionInstruction.SELL_TO_OPEN, 'XYZ_011819P45', 1)
+            .add_option_leg(OptionInstruction.BUY_TO_OPEN, 'XYZ_011720P43', 2)
+            .set_complex_order_strategy_type(ComplexOrderStrategyType.CUSTOM)
+            .set_duration(Duration.DAY)
+            .set_session(Session.NORMAL))
+
+        expected = {
+            'orderStrategyType': 'SINGLE',
+            'orderType': 'MARKET',
+            'orderLegCollection': [
+                {
+                    'instrument': {
+                        'assetType': 'OPTION',
+                        'symbol': 'XYZ_011819P45'
+                    },
+                    'instruction': 'SELL_TO_OPEN',
+                    'quantity': 1
+                },
+                {
+                    'instrument': {
+                        'assetType': 'OPTION',
+                        'symbol': 'XYZ_011720P43'
+                    },
+                    'instruction': 'BUY_TO_OPEN',
+                    'quantity': 2
+                }
+            ],
+            'complexOrderStrategyType': 'CUSTOM',
+            'duration': 'DAY',
+            'session': 'NORMAL'
+        }
+
+        self.assertFalse(has_diff(expected, builder.build()))
+
+    @no_duplicates
+    def test_conditional_order_one_triggers_another(self):
+        builder = (
+            OrderBuilder()
+            .set_order_type(OrderType.LIMIT)
+            .set_session(Session.NORMAL)
+            .set_price(34.97)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.TRIGGER)
+            .add_equity_leg(EquityInstruction.BUY, 'XYZ', 10)
+            .add_child_order_strategy(
+                OrderBuilder()
+                .set_order_type(OrderType.LIMIT)
+                .set_session(Session.NORMAL)
+                .set_price(42.03)
+                .set_duration(Duration.DAY)
+                .set_order_strategy_type(OrderStrategyType.SINGLE)
+                .add_equity_leg(EquityInstruction.SELL, 'XYZ', 10)))
+
+        expected = {
+            'orderType': 'LIMIT',
+            'session': 'NORMAL',
+            'price': '34.97',
+            'duration': 'DAY',
+            'orderStrategyType': 'TRIGGER',
+            'orderLegCollection': [
+                {
+                    'instruction': 'BUY',
+                    'quantity': 10,
+                    'instrument': {
+                        'symbol': 'XYZ',
+                        'assetType': 'EQUITY'
+                    }
+                }
+            ],
+            'childOrderStrategies': [
+                {
+                    'orderType': 'LIMIT',
+                    'session': 'NORMAL',
+                    'price': '42.03',
+                    'duration': 'DAY',
+                    'orderStrategyType': 'SINGLE',
+                    'orderLegCollection': [
+                        {
+                            'instruction': 'SELL',
+                            'quantity': 10,
+                            'instrument': {
+                                'symbol': 'XYZ',
+                                'assetType': 'EQUITY'
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+
+        self.assertFalse(has_diff(expected, builder.build()))
+
+    @no_duplicates
+    def test_conditional_order_one_cancels_another(self):
+        builder = (
+            OrderBuilder()
+            .set_order_strategy_type(OrderStrategyType.OCO)
+            .add_child_order_strategy(
+                OrderBuilder()
+                .set_order_type(OrderType.LIMIT)
+                .set_session(Session.NORMAL)
+                .set_price(45.97)
+                .set_duration(Duration.DAY)
+                .set_order_strategy_type(OrderStrategyType.SINGLE)
+                .add_equity_leg(EquityInstruction.SELL, 'XYZ', 2)
+            )
+            .add_child_order_strategy(
+                OrderBuilder()
+                .set_order_type(OrderType.STOP_LIMIT)
+                .set_session(Session.NORMAL)
+                .set_price(37.00)
+                .set_stop_price(37.03)
+                .set_duration(Duration.DAY)
+                .set_order_strategy_type(OrderStrategyType.SINGLE)
+                .add_equity_leg(EquityInstruction.SELL, 'XYZ', 2)))
+
+        expected = {
+            'orderStrategyType': 'OCO',
+            'childOrderStrategies': [
+                {
+                    'orderType': 'LIMIT',
+                    'session': 'NORMAL',
+                    'price': '45.97',
+                    'duration': 'DAY',
+                    'orderStrategyType': 'SINGLE',
+                    'orderLegCollection': [
+                        {
+                            'instruction': 'SELL',
+                            'quantity': 2,
+                            'instrument': {
+                                'symbol': 'XYZ',
+                                'assetType': 'EQUITY'
+                            }
+                        }
+                    ]
+                },
+                {
+                    'orderType': 'STOP_LIMIT',
+                    'session': 'NORMAL',
+                    'price': '37.00',
+                    'stopPrice': '37.03',
+                    'duration': 'DAY',
+                    'orderStrategyType': 'SINGLE',
+                    'orderLegCollection': [
+                        {
+                            'instruction': 'SELL',
+                            'quantity': 2,
+                            'instrument': {
+                                'symbol': 'XYZ',
+                                'assetType': 'EQUITY'
+                            }
+                        }
+                    ]
+                }
+            ]
+        }
+
+        self.assertFalse(has_diff(expected, builder.build()))
+
+    @no_duplicates
+    def test_conditional_order_one_triggers_a_one_cancels_other(self):
+        builder = (
+            OrderBuilder()
+            .set_order_strategy_type(OrderStrategyType.TRIGGER)
+            .set_session(Session.NORMAL)
+            .set_duration(Duration.DAY)
+            .set_order_type(OrderType.LIMIT)
+            .set_price(14.97)
+            .add_equity_leg(EquityInstruction.BUY, 'XYZ', 5)
+            .add_child_order_strategy(
+                OrderBuilder()
+                .set_order_strategy_type(OrderStrategyType.OCO)
+                .add_child_order_strategy(
+                    OrderBuilder()
+                    .set_order_strategy_type(OrderStrategyType.SINGLE)
+                    .set_session(Session.NORMAL)
+                    .set_duration(Duration.GOOD_TILL_CANCEL)
+                    .set_order_type(OrderType.LIMIT)
+                    .set_price(15.27)
+                    .add_equity_leg(EquityInstruction.SELL, 'XYZ', 5))
+                .add_child_order_strategy(
+                    OrderBuilder()
+                    .set_order_strategy_type(OrderStrategyType.SINGLE)
+                    .set_session(Session.NORMAL)
+                    .set_duration(Duration.GOOD_TILL_CANCEL)
+                    .set_order_type(OrderType.STOP)
+                    .set_stop_price(11.27)
+                    .add_equity_leg(EquityInstruction.SELL, 'XYZ', 5))))
+
+        expected = {
+            'orderStrategyType': 'TRIGGER',
+            'session': 'NORMAL',
+            'duration': 'DAY',
+            'orderType': 'LIMIT',
+            'price': '14.97',
+            'orderLegCollection': [
+                {
+                    'instruction': 'BUY',
+                    'quantity': 5,
+                    'instrument': {
+                        'assetType': 'EQUITY',
+                        'symbol': 'XYZ'
+                    }
+                }
+            ],
+            'childOrderStrategies': [
+                {
+                    'orderStrategyType': 'OCO',
+                    'childOrderStrategies': [
+                        {
+                            'orderStrategyType': 'SINGLE',
+                            'session': 'NORMAL',
+                            'duration': 'GOOD_TILL_CANCEL',
+                            'orderType': 'LIMIT',
+                            'price': '15.27',
+                            'orderLegCollection': [
+                                {
+                                    'instruction': 'SELL',
+                                    'quantity': 5,
+                                    'instrument': {
+                                        'assetType': 'EQUITY',
+                                        'symbol': 'XYZ'
+                                    }
+                                }
+                            ]
+                        },
+                        {
+                            'orderStrategyType': 'SINGLE',
+                            'session': 'NORMAL',
+                            'duration': 'GOOD_TILL_CANCEL',
+                            'orderType': 'STOP',
+                            'stopPrice': '11.27',
+                            'orderLegCollection': [
+                                {
+                                    'instruction': 'SELL',
+                                    'quantity': 5,
+                                    'instrument': {
+                                        'assetType': 'EQUITY',
+                                        'symbol': 'XYZ'
+                                    }
+                                }
+                            ]
+                        }
+                    ]
+                }
+            ]
+        }
+
+        self.assertFalse(has_diff(expected, builder.build()))
+
+    @no_duplicates
+    def test_sell_trailing_stop_stock(self):
+        builder = (
+            OrderBuilder()
+            .set_complex_order_strategy_type(ComplexOrderStrategyType.NONE)
+            .set_order_type(OrderType.TRAILING_STOP)
+            .set_session(Session.NORMAL)
+            .set_stop_price_link_basis(StopPriceLinkBasis.BID)
+            .set_stop_price_link_type(StopPriceLinkType.VALUE)
+            .set_stop_price_offset(10)
+            .set_duration(Duration.DAY)
+            .set_order_strategy_type(OrderStrategyType.SINGLE)
+            .add_equity_leg(EquityInstruction.SELL, 'XYZ', 10))
+
+        expected = {
+            'complexOrderStrategyType': 'NONE',
+            'orderType': 'TRAILING_STOP',
+            'session': 'NORMAL',
+            'stopPriceLinkBasis': 'BID',
+            'stopPriceLinkType': 'VALUE',
+            'stopPriceOffset': 10,
+            'duration': 'DAY',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [
+                {
+                    'instruction': 'SELL',
+                    'quantity': 10,
+                    'instrument': {
+                        'symbol': 'XYZ',
+                        'assetType': 'EQUITY'
+                    }
+                }
+            ]
+        }
+
+        self.assertFalse(has_diff(expected, builder.build()))
diff --git a/tests/orders_test.py b/tests/orders_test.py
index 541a8d7..37bb8a6 100644
--- a/tests/orders_test.py
+++ b/tests/orders_test.py
@@ -1,8 +1,8 @@
 import unittest
 
-from tda.orders import Duration, EquityOrderBuilder, InvalidOrderException
-from tda.orders import Session
-from tests.test_utils import no_duplicates
+from tda.orders.common import *
+from tda.orders.equities import *
+from tests.test_utils import has_diff, no_duplicates
 from . import test_utils
 
 
@@ -99,3 +99,138 @@ class EquityOrderBuilderTest(unittest.TestCase):
     @no_duplicates
     def test_instruction_required(self):
         self.field_required('instruction')
+
+
+class BuilderTemplates(unittest.TestCase):
+
+    def test_equity_buy_market(self):
+        self.assertFalse(has_diff({
+            'orderType': 'MARKET',
+            'session': 'NORMAL',
+            'duration': 'DAY',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [{
+                'instruction': 'BUY',
+                'quantity': 10,
+                'instrument': {
+                    'symbol': 'GOOG',
+                    'assetType': 'EQUITY',
+                }
+            }]
+        }, equity_buy_market('GOOG', 10).build()))
+
+    def test_equity_buy_limit(self):
+        self.assertFalse(has_diff({
+            'orderType': 'LIMIT',
+            'session': 'NORMAL',
+            'duration': 'DAY',
+            'price': '199.99',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [{
+                'instruction': 'BUY',
+                'quantity': 10,
+                'instrument': {
+                    'symbol': 'GOOG',
+                    'assetType': 'EQUITY',
+                }
+            }]
+        }, equity_buy_limit('GOOG', 10, 199.99).build()))
+
+    def test_equity_sell_market(self):
+        self.assertFalse(has_diff({
+            'orderType': 'MARKET',
+            'session': 'NORMAL',
+            'duration': 'DAY',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [{
+                'instruction': 'SELL',
+                'quantity': 10,
+                'instrument': {
+                    'symbol': 'GOOG',
+                    'assetType': 'EQUITY',
+                }
+            }]
+        }, equity_sell_market('GOOG', 10).build()))
+
+    def test_equity_sell_limit(self):
+        self.assertFalse(has_diff({
+            'orderType': 'LIMIT',
+            'session': 'NORMAL',
+            'duration': 'DAY',
+            'price': '199.99',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [{
+                'instruction': 'SELL',
+                'quantity': 10,
+                'instrument': {
+                    'symbol': 'GOOG',
+                    'assetType': 'EQUITY',
+                }
+            }]
+        }, equity_sell_limit('GOOG', 10, 199.99).build()))
+
+    def test_equity_sell_short_market(self):
+        self.assertFalse(has_diff({
+            'orderType': 'MARKET',
+            'session': 'NORMAL',
+            'duration': 'DAY',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [{
+                'instruction': 'SELL_SHORT',
+                'quantity': 10,
+                'instrument': {
+                    'symbol': 'GOOG',
+                    'assetType': 'EQUITY',
+                }
+            }]
+        }, equity_sell_short_market('GOOG', 10).build()))
+
+    def test_equity_sell_short_limit(self):
+        self.assertFalse(has_diff({
+            'orderType': 'LIMIT',
+            'session': 'NORMAL',
+            'duration': 'DAY',
+            'price': '199.99',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [{
+                'instruction': 'SELL_SHORT',
+                'quantity': 10,
+                'instrument': {
+                    'symbol': 'GOOG',
+                    'assetType': 'EQUITY',
+                }
+            }]
+        }, equity_sell_short_limit('GOOG', 10, 199.99).build()))
+
+    def test_equity_buy_to_cover_market(self):
+        self.assertFalse(has_diff({
+            'orderType': 'MARKET',
+            'session': 'NORMAL',
+            'duration': 'DAY',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [{
+                'instruction': 'BUY_TO_COVER',
+                'quantity': 10,
+                'instrument': {
+                    'symbol': 'GOOG',
+                    'assetType': 'EQUITY',
+                }
+            }]
+        }, equity_buy_to_cover_market('GOOG', 10).build()))
+
+    def test_equity_buy_to_cover_limit(self):
+        self.assertFalse(has_diff({
+            'orderType': 'LIMIT',
+            'session': 'NORMAL',
+            'duration': 'DAY',
+            'price': '199.99',
+            'orderStrategyType': 'SINGLE',
+            'orderLegCollection': [{
+                'instruction': 'BUY_TO_COVER',
+                'quantity': 10,
+                'instrument': {
+                    'symbol': 'GOOG',
+                    'assetType': 'EQUITY',
+                }
+            }]
+        }, equity_buy_to_cover_limit('GOOG', 10, 199.99).build()))
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 2c9cabf..af49be0 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -75,8 +75,8 @@ class MockResponse:
 
 
 def has_diff(old, new):
-    old_out = json.dumps(old, indent=4).splitlines()
-    new_out = json.dumps(new, indent=4).splitlines()
+    old_out = json.dumps(old, indent=4, sort_keys=True).splitlines()
+    new_out = json.dumps(new, indent=4, sort_keys=True).splitlines()
     diff = difflib.ndiff(old_out, new_out)
     diff, has_diff = color_diff(diff)