Skip to content

Commit

Permalink
Merge pull request alpacahq#410 from alpacahq/feature/fractional-support
Browse files Browse the repository at this point in the history
ENG-4310: Add Fractional Share Support
  • Loading branch information
ducille authored Apr 8, 2021
2 parents a3354fa + 0ae52d3 commit e77ef77
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 17 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ You can access the following information through this object.
| get_account() | `GET /account` and | `Account` entity.|
| get_order_by_client_order_id(client_order_id) | `GET /orders` with client_order_id | `Order` entity.|
| list_orders(status=None, limit=None, after=None, until=None, direction=None, nested=None) | `GET /orders` | list of `Order` entities. `after` and `until` need to be string format, which you can obtain by `pd.Timestamp().isoformat()` |
| submit_order(symbol, qty, side, type, time_in_force, limit_price=None, stop_price=None, client_order_id=None, order_class=None, take_profit=None, stop_loss=None, trail_price=None, trail_percent=None)| `POST /orders` | `Order` entity. |
| submit_order(symbol, qty=None, side="buy", type="market", time_in_force="day", limit_price=None, stop_price=None, client_order_id=None, order_class=None, take_profit=None, stop_loss=None, trail_price=None, trail_percent=None, notional=None)| `POST /orders` | `Order` entity. |
| get_order(order_id) | `GET /orders/{order_id}` | `Order` entity.|
| cancel_order(order_id) | `DELETE /orders/{order_id}` | |
| cancel_all_orders() | `DELETE /orders`| |
Expand Down Expand Up @@ -295,6 +295,28 @@ api.submit_order(
)
```

For simple orders with `type='market'` and `time_in_force='day'`, you can pass a fractional amount (`qty`) or a `notional` amount (but not both). For instace, if the current market price for SPY is $300, the following calls are equivalent:

```py
api.submit_order(
symbol='SPY',
qty=1.5, # fractional shares
side='buy',
type='market',
time_in_force='day',
)
```

```py
api.submit_order(
symbol='SPY',
notional=450, # notional value of 1.5 shares of SPY at $300
side='buy',
type='market',
time_in_force='day',
)
```

##### Using `get_barset()` (Deprecated. use `get_bars()` instead)
```python
import pandas as pd
Expand Down
21 changes: 13 additions & 8 deletions alpaca_trade_api/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,10 @@ def list_orders(self,

def submit_order(self,
symbol: str,
qty: int,
side: str,
type: str,
time_in_force: str,
qty: float = None,
side: str = "buy",
type: str = "market",
time_in_force: str = "day",
limit_price: str = None,
stop_price: str = None,
client_order_id: str = None,
Expand All @@ -281,10 +281,11 @@ def submit_order(self,
take_profit: dict = None,
stop_loss: dict = None,
trail_price: str = None,
trail_percent: str = None):
trail_percent: str = None,
notional: float = None):
"""
:param symbol: symbol or asset ID
:param qty: int
:param qty: float. Mutually exclusive with "notional".
:param side: buy or sell
:param type: market, limit, stop, stop_limit or trailing_stop
:param time_in_force: day, gtc, opg, cls, ioc, fok
Expand All @@ -300,15 +301,19 @@ def submit_order(self,
{"stop_price": "297.95", "limit_price": "298.95"}
:param trail_price: str of float
:param trail_percent: str of float
:param notional: float. Mutually exclusive with "qty".
"""
"""Request a new order"""
params = {
'symbol': symbol,
'qty': qty,
'side': side,
'type': type,
'time_in_force': time_in_force
}
if qty is not None:
params['qty'] = qty
if notional is not None:
params['notional'] = notional
if limit_price is not None:
params['limit_price'] = FLOAT(limit_price)
if stop_price is not None:
Expand Down Expand Up @@ -360,7 +365,7 @@ def replace_order(
stop_price: str = None,
trail: str = None,
time_in_force: str = None,
client_order_id: str = None
client_order_id: str = None,
) -> Order:
"""
:param order_id:
Expand Down
138 changes: 130 additions & 8 deletions tests/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def test_orders(reqmock):
"account_id": "904837e3-3b76-47ec-b432-046db621571b",
"asset_id": "904837e3-3b76-47ec-b432-046db621571b",
"qty": "15",
"notional": null,
"side": "buy",
"type": "market",
"timeinforce": "day",
Expand Down Expand Up @@ -134,6 +135,7 @@ def test_orders(reqmock):
"account_id": "904837e3-3b76-47ec-b432-046db621571b",
"asset_id": "904837e3-3b76-47ec-b432-046db621571b",
"qty": "15",
"notional": null,
"side": "buy",
"type": "market",
"timeinforce": "day",
Expand Down Expand Up @@ -166,6 +168,7 @@ def test_orders(reqmock):
client_order_id='904837e3-3b76-47ec-b432-046db621571b',
)
assert order.qty == "15"
assert order.notional is None
assert order.created_at.hour == 19
assert type(order) == tradeapi.entity.Order
assert type(api_raw.submit_order(
Expand Down Expand Up @@ -232,6 +235,7 @@ def test_orders(reqmock):
"account_id": "904837e3-3b76-47ec-b432-046db621571b",
"asset_id": "904837e3-3b76-47ec-b432-046db621571b",
"qty": "15",
"notional": null,
"side": "buy",
"type": "market",
"timeinforce": "day",
Expand Down Expand Up @@ -268,6 +272,7 @@ def test_orders(reqmock):
"account_id": "904837e3-3b76-47ec-b432-046db621571b",
"asset_id": "904837e3-3b76-47ec-b432-046db621571b",
"qty": 15,
"notional": null,
"side": "buy",
"type": "market",
"timeinforce": "day",
Expand Down Expand Up @@ -757,16 +762,16 @@ def test_watchlists(reqmock):


def test_errors(reqmock):
api = tradeapi.REST('key-id', 'secret-key', api_version='v1')
api_v1 = tradeapi.REST('key-id', 'secret-key', api_version='v1')

api._retry = 1
api._retry_wait = 0
api_v1._retry = 1
api_v1._retry_wait = 0

api._do_error = True
api_v1._do_error = True

def callback_429(request, context):
if api._do_error:
api._do_error = False
if api_v1._do_error:
api_v1._do_error = False
context.status_code = 429
return 'Too Many Requests'
else:
Expand All @@ -792,7 +797,7 @@ def callback_429(request, context):
text=callback_429,
)

account = api.get_account()
account = api_v1.get_account()
assert account.cash == '4000.32'

# General API Error
Expand All @@ -805,7 +810,7 @@ def callback_429(request, context):
)

try:
api.submit_order(
api_v1.submit_order(
symbol='AAPL',
side='buy',
qty='3',
Expand All @@ -820,6 +825,123 @@ def callback_429(request, context):
else:
assert False

api_v2 = tradeapi.REST('key-id', 'secret-key', api_version='v2')

# `qty` and `notional` both null in submit_order
reqmock.post(
'https://api.alpaca.markets/v2/orders',
status_code=422,
text='''
{"code":40010001,"message":"qty or notional is required"}
'''
)

try:
api_v2.submit_order(
symbol='AAPL',
side='buy',
type='market',
time_in_force='day',
)
except APIError as err:
assert err.code == 40010001
assert err.status_code == 422
assert err.request is not None
assert err.response.status_code == err.status_code
else:
assert False

# `qty` and `notional` both non-null in submit_order
reqmock.post(
'https://api.alpaca.markets/v2/orders',
status_code=422,
text='''
{"code": 40010001, "message": "only one of qty or notional is accepted"}
'''
)

try:
api_v2.submit_order(
symbol='AAPL',
side='buy',
type='market',
time_in_force='day',
qty=1,
notional=1,
)
except APIError as err:
assert err.code == 40010001
assert err.status_code == 422
assert err.request is not None
assert err.response.status_code == err.status_code
else:
assert False

# fractional `qty` passed to replace_order
reqmock.post(
'https://api.alpaca.markets/v2/orders',
text='''{
"id": "fb61d316-2179-4df2-8b28-eb026c0dd78e",
"client_order_id": "6de7d1b2-f772-4a0d-8c15-bacea15eb29e",
"created_at": "2021-04-07T18:25:30.812371Z",
"updated_at": "2021-04-07T18:25:30.812371Z",
"submitted_at": "2021-04-07T18:25:30.803178Z",
"filled_at": null,
"expired_at": null,
"canceled_at": null,
"failed_at": null,
"replaced_at": null,
"replaced_by": null,
"replaces": null,
"asset_id": "b28f4066-5c6d-479b-a2af-85dc1a8f16fb",
"symbol": "SPY",
"asset_class": "us_equity",
"notional": null,
"qty": "1",
"filled_qty": "0",
"filled_avg_price": null,
"order_class": "",
"order_type": "limit",
"type": "limit",
"side": "buy",
"time_in_force": "day",
"limit_price": "400",
"stop_price": null,
"status": "accepted",
"extended_hours": false,
"legs": null,
"trail_percent": null,
"trail_price": null,
"hwm": null
}''')
order = api_v2.submit_order(
symbol='SPY',
qty=1,
side='buy',
type='limit',
time_in_force='day',
limit_price='400.00',
)

reqmock.patch(
'https://api.alpaca.markets/v2/orders/{}'.format(order.id),
status_code=422,
text='''{
"code": 40010001,
"message": "qty must be integer"
}''')
try:
api_v2.replace_order(
order_id=order.id,
qty="1.5",
client_order_id=order.client_order_id,
)
except APIError as err:
assert err.code == 40010001
assert err.status_code == 422
assert err.request is not None
assert err.response.status_code == err.status_code


def test_no_resource_warning_with_context_manager():
with pytest.warns(None) as record: # ensure no warnings are raised
Expand Down

0 comments on commit e77ef77

Please sign in to comment.