Skip to content

Commit

Permalink
add more apis for placing order; fix get_order end time; improve test…
Browse files Browse the repository at this point in the history
…s; updated API reference and README
  • Loading branch information
hzheng committed May 24, 2024
1 parent f3a5a52 commit a25d020
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 71 deletions.
15 changes: 5 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,21 @@ print(access_token)
trading_api = TradingApi(access_token, app_config['trading'])

account_num = 0 # CHANGE this to your actual account number
trading_data = trading_api.fetch_trading_data(account_num)
trading_api.set_current_account_number(account_num)
trading_data = trading_api.fetch_trading_data()
# List positions
for position in trading_data.positions:
print("position:", position)

# List transactions
for transaction in trading_api.get_transactions(account_num):
for transaction in trading_api.get_transactions():
print("transaction:", transaction)

# Place order
order_dict = {
"orderType": "LIMIT", "session": "NORMAL", "duration": "DAY", "orderStrategyType": "SINGLE", "price": '100.00',
"orderLegCollection": [
{"instruction": "BUY", "quantity": 1, "instrument": {"symbol": "TSLA", "assetType": "EQUITY"}}
]
}
trading_api.place_order(order_dict, account_num)
trading_api.buy_equity("TSLA", quantity=1, price=100)

# List orders
for order in trading_api.get_orders(account_num):
for order in trading_api.get_orders():
print("order:", order)

# Example usage of market APIs
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyschwab"
version = "0.0.1a3"
version = "0.0.1a4"
description = "A Python library for the Schwab trading API"
authors = ["Hui Zheng <[email protected]>"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion pyschwab/market.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"""
Schwab Market API
Reference: https://beta-developer.schwab.com/products/trader-api--individual/details/specifications/Market%20Data%20Production
Reference: https://developer.schwab.com/products/trader-api--individual/details/specifications/Market%20Data%20Production
"""
class MarketApi:
base_market_url: str
Expand Down
72 changes: 56 additions & 16 deletions pyschwab/trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@

from dotenv import load_dotenv

from .utils import format_params, request, time_to_str, to_json_str
from .trading_models import Order, SecuritiesAccount, TradingData, Transaction, UserPreference
from .types import AssetType, MarketSession, OrderDuration, OrderInstruction, OrderStatus, OrderStrategyType, OrderType, TransactionType
from .utils import format_params, remove_none_values, request, time_to_str, to_json_str
from .trading_models import Instrument, Order, OrderLeg, SecuritiesAccount, TradingData, Transaction, UserPreference
from .types import AssetType, ExecutionType, MarketSession, OrderDuration, OrderInstruction, OrderStatus, OrderStrategyType, OrderType, TransactionType


"""
Schwab Trading API
Reference: https://beta-developer.schwab.com/products/trader-api--individual/details/specifications/Retail%20Trader%20API%20Production
Reference: https://developer.schwab.com/products/trader-api--individual/details/specifications/Retail%20Trader%20API%20Production
"""
class TradingApi:
base_trader_url: str
Expand Down Expand Up @@ -78,23 +78,37 @@ def fetch_all_trading_data(self, include_pos: bool=True) -> Dict[str, TradingDat
self.accounts[account_num] = account
return trading_data_map

def get_all_orders(self, start_time: datetime=None, end_time: datetime=None, status: OrderStatus=None, max_results: int=100) -> List[Order]:
def get_all_orders(self, start_time: datetime=None, end_time: datetime=None, status: OrderStatus=None, max_results: int=None) -> List[Order]:
now = datetime.now()
start = time_to_str(start_time or now - timedelta(days=30))
end = time_to_str(end_time or now)
start = time_to_str(start_time or now - timedelta(days=60))
end = time_to_str(end_time or now + timedelta(days=1))
params = {'maxResults': max_results, 'fromEnteredTime': start, 'toEnteredTime': end, 'status': status}
orders = request(f'{self.base_trader_url}/orders', headers=self.auth, params=format_params(params)).json()
return [Order.from_dict(order) for order in orders]

def get_orders(self, start_time: datetime=None, end_time: datetime=None, status: OrderStatus=None, max_results: int=100) -> List[Order]:
def get_orders(self, start_time: datetime=None, end_time: datetime=None, status: OrderStatus=None, max_results: int=None) -> List[Order]:
account_hash = self._get_account_hash()
now = datetime.now()
start = time_to_str(start_time or now - timedelta(days=30))
end = time_to_str(end_time or now)
start = time_to_str(start_time or now - timedelta(days=60))
end = time_to_str(end_time or now + timedelta(days=1))
params = {'maxResults': max_results, 'fromEnteredTime': start, 'toEnteredTime': end, 'status': status}
orders = request(f'{self.base_trader_url}/accounts/{account_hash}/orders', headers=self.auth, params=format_params(params)).json()
return [Order.from_dict(order) for order in orders]

@staticmethod
def is_active_order(order: Order) -> bool:
if order.status.is_inactive():
return False

activities = order.order_activity_collection
if activities:
if activities[0].execution_type == ExecutionType.CANCELED:
return False
return order.cancel_time is None

def get_working_orders(self, start_time: datetime=None, end_time: datetime=None) -> List[Order]:
return self.get_orders(start_time, end_time, status=OrderStatus.WORKING)

def get_order(self, order_id: str, account_num: int | str=None) -> Order:
account_hash = self._get_account_hash(account_num)
order = request(f'{self.base_trader_url}/accounts/{account_hash}/orders/{order_id}', headers=self.auth).json()
Expand All @@ -104,6 +118,31 @@ def place_order(self, order: Dict[str, Any] | Order) -> None:
account_hash = self._get_account_hash()
order_dict = self._convert_order(order)
request(f'{self.base_trader_url}/accounts/{account_hash}/orders', method='POST', headers=self.auth, json=order_dict)

def place_complex_order(self, price: float, leg_collection: List[OrderLeg],
strategy_type: OrderStrategyType=OrderStrategyType.SINGLE, order_type: OrderType=OrderType.LIMIT,
duration: OrderDuration=OrderDuration.DAY, session: MarketSession=MarketSession.NORMAL) -> None:
order = Order(price=price, order_leg_collection=leg_collection, order_strategy_type=strategy_type,
order_type=order_type, duration=duration, session=session)
self.place_order(order)

def place_single_order(self, symbol: str, quantity: int, price: float, instruction: OrderInstruction,
asset_type: AssetType=AssetType.EQUITY, order_type: OrderType=OrderType.LIMIT,
duration: OrderDuration=OrderDuration.DAY, session: MarketSession=MarketSession.NORMAL) -> None:
instrument = Instrument(symbol=symbol, asset_type=asset_type)
leg_collection = [OrderLeg(instruction=instruction, quantity=quantity, instrument=instrument)]
self.place_complex_order(price=price, leg_collection=leg_collection, strategy_type=OrderStrategyType.SINGLE,
order_type=order_type, duration=duration, session=session)

def buy_equity(self, symbol: str, quantity: int, price: float, order_type: OrderType=OrderType.LIMIT,
duration: OrderDuration=OrderDuration.DAY, session: MarketSession=MarketSession.NORMAL) -> None:
self.place_single_order(symbol=symbol, quantity=quantity, price=price, instruction=OrderInstruction.BUY,
order_type=order_type, duration=duration, session=session)

def sell_equity(self, symbol: str, price: float, quantity: int, order_type: OrderType=OrderType.LIMIT,
duration: OrderDuration=OrderDuration.DAY, session: MarketSession=MarketSession.NORMAL) -> None:
self.place_single_order(symbol=symbol, quantity=quantity, price=price, instruction=OrderInstruction.SELL,
order_type=order_type, duration=duration, session=session)

def cancel_order(self, order_id: int | str) -> None:
account_hash = self._get_account_hash()
Expand All @@ -127,13 +166,14 @@ def preview_order(self, order: Dict[str, Any] | Order) -> Dict[str, Any]:
return request(f'{self.base_trader_url}/accounts/{account_hash}/previewOrder', method='POST', headers=self.auth2, data=order_json).json()

def _convert_order(self, order: Dict[str, Any] | Order) -> Dict[str, Any]:
order_dict = None
if isinstance(order, Dict):
return order

if isinstance(order, Order):
return order.to_dict(clean_keys=True)

raise ValueError("Order must be a dictionary or Order object.")
order_dict = order
elif isinstance(order, Order):
order_dict = order.to_dict(clean_keys=True)
else:
raise ValueError("Order must be a dictionary or Order object.")
return remove_none_values(order_dict)

def get_transactions(self, start_time: datetime=None, end_time: datetime=None, symbol: str=None, types: TransactionType=TransactionType.TRADE) -> List[Transaction]:
account_hash = self._get_account_hash()
Expand Down
54 changes: 28 additions & 26 deletions pyschwab/trading_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import datetime
from typing import Any, Dict, List, Optional

from .types import AssetType, ComplexOrderStrategyType, MarketSession, OptionActionType, \
from .types import ActivityType, AssetType, ComplexOrderStrategyType, ExecutionType, MarketSession, OptionActionType, \
OptionAssetType, OrderDuration, OrderInstruction, OrderStatus, OrderStrategyType, OrderType, \
PositionEffect, QuantityType, RequestedDestination
from .utils import camel_to_snake, dataclass_to_dict, to_time
Expand Down Expand Up @@ -87,16 +87,16 @@ class Instrument:
symbol: str
asset_type: AssetType = AssetType.EQUITY
cusip: str = None
net_change: float = 0.0
instrument_id: int = 0
net_change: float = None
instrument_id: int = None
description: str = None
closing_price: float = 0.0
closing_price: float = None
status: str = None
type: str = None
expiration_date: datetime = None
option_deliverables: List[Deliverable] = None
option_premium_multiplier: float = 0.0
put_call: OptionActionType = OptionActionType.CALL
option_premium_multiplier: float = None
put_call: OptionActionType = None
strike_price: float = None
underlying_symbol: str = None
underlying_cusip: str = None
Expand Down Expand Up @@ -292,15 +292,17 @@ def from_dict(cls, data: Dict[str, Any]) -> 'ExecutionLeg':

@dataclass
class OrderActivity:
activity_type: str
execution_type: str
activity_type: ActivityType
execution_type: ExecutionType
quantity: int
order_remaining_quantity: int
execution_legs: List[ExecutionLeg]

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'OrderActivity':
converted_data = {camel_to_snake(key): value for key, value in data.items()}
converted_data['activity_type'] = ActivityType.from_str(converted_data.get('activity_type', None))
converted_data['execution_type'] = ExecutionType.from_str(converted_data.get('execution_type', None))
converted_data['execution_legs'] = [ExecutionLeg.from_dict(leg) for leg in converted_data['execution_legs']]
return cls(**converted_data)

Expand All @@ -310,9 +312,9 @@ class OrderLeg:
instrument: Instrument
instruction: OrderInstruction
quantity: int
position_effect: PositionEffect = PositionEffect.OPENING
order_leg_type: AssetType = AssetType.EQUITY
quantity_type: QuantityType = QuantityType.ALL_SHARES
position_effect: PositionEffect = None
order_leg_type: AssetType = None
quantity_type: QuantityType = None
leg_id: int = None
div_cap_gains: str = None
to_symbol: str = None
Expand All @@ -331,36 +333,36 @@ def from_dict(cls, data: Dict[str, Any]) -> 'OrderLeg':
@dataclass
class Order:
price: float
entered_time: datetime
close_time: datetime
release_time: datetime
entered_time: datetime = None
close_time: datetime = None
release_time: datetime = None
order_type: OrderType = OrderType.LIMIT
duration: OrderDuration = OrderDuration.DAY
session: MarketSession = MarketSession.NORMAL
order_strategy_type: OrderStrategyType = OrderStrategyType.SINGLE
order_id: int = 0
account_number: int = 0
status: OrderStatus = OrderStatus.AWAITING_PARENT_ORDER
quantity: int = 0
filled_quantity: int = 0
remaining_quantity: int = 0
complex_order_strategy_type: ComplexOrderStrategyType = ComplexOrderStrategyType.NONE
requested_destination: RequestedDestination = RequestedDestination.AUTO
order_id: int = None
account_number: int = None
status: OrderStatus = None
quantity: int = None
filled_quantity: int = None
remaining_quantity: int = None
complex_order_strategy_type: ComplexOrderStrategyType = None
requested_destination: RequestedDestination = None
destination_link_name: str = None
order_leg_collection: List[OrderLeg] = None
order_activity_collection: List[OrderActivity] = None
stop_price: float = 0.0
stop_price: float = None
stop_price_link_basis: str = None
stop_price_link_type: str = None
stop_price_offset: float = None
stop_type: str = None
price_link_basis: str = None
price_link_type: str = None
tax_lot_method: str = None
activation_price: float = 0.0
activation_price: float = None
special_instruction: str = None
cancelable: bool = False
editable: bool = False
cancelable: bool = None
editable: bool = None
cancel_time: datetime = None
tag: str = None
status_description: str = None
Expand Down
13 changes: 13 additions & 0 deletions pyschwab/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ class OrderStatus(AutoName):
PENDING_RECALL = auto()
UNKNOWN = auto()

def is_inactive(self):
return self in [OrderStatus.CANCELED, OrderStatus.REJECTED, OrderStatus.EXPIRED, OrderStatus.FILLED]


class RequestedDestination(AutoName):
INET = auto()
Expand Down Expand Up @@ -337,6 +340,16 @@ class OptionStrategy(AutoName):
ROLL = auto()


class ActivityType(AutoName):
EXECUTION = auto()
ORDER_ACTION = auto()


class ExecutionType(AutoName):
FILL = auto()
CANCELED = auto()


class MoverSort(AutoName):
VOLUME = auto()
TRADES = auto()
Expand Down
28 changes: 28 additions & 0 deletions pyschwab/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,34 @@ def format_list(lst: str | List) -> str:

return ",".join(lst)

def remove_none_values(data: Dict[str, Any]) -> Dict[str, Any]:
"""
Recursively remove all keys with None values from the dictionary.
Args:
data (dict): The dictionary to clean.
Returns:
dict: The cleaned dictionary with no None values.
"""
if not isinstance(data, dict):
return data

clean_dict = {}
for key, value in data.items():
if isinstance(value, dict):
nested_clean_dict = remove_none_values(value)
if nested_clean_dict: # only add non-empty nested dictionaries
clean_dict[key] = nested_clean_dict
elif isinstance(value, list):
clean_list = [remove_none_values(item) for item in value]
if clean_list: # only add non-empty lists
clean_dict[key] = clean_list
elif value is not None:
clean_dict[key] = value

return clean_dict


def to_json_str(obj):
def json_encode(obj):
Expand Down
Loading

0 comments on commit a25d020

Please sign in to comment.