MarqSpec.Client.ProjectX
3.0.0
dotnet add package MarqSpec.Client.ProjectX --version 3.0.0
NuGet\Install-Package MarqSpec.Client.ProjectX -Version 3.0.0
<PackageReference Include="MarqSpec.Client.ProjectX" Version="3.0.0" />
<PackageVersion Include="MarqSpec.Client.ProjectX" Version="3.0.0" />
<PackageReference Include="MarqSpec.Client.ProjectX" />
paket add MarqSpec.Client.ProjectX --version 3.0.0
#r "nuget: MarqSpec.Client.ProjectX, 3.0.0"
#:package MarqSpec.Client.ProjectX@3.0.0
#addin nuget:?package=MarqSpec.Client.ProjectX&version=3.0.0
#tool nuget:?package=MarqSpec.Client.ProjectX&version=3.0.0
ProjectX API Client
A .NET client library for the ProjectX REST API, providing easy access to market data and trading operations.
Features
✅ User Story 1: Authentication
- API key and secret authentication via environment variables or configuration files
- Automatic JWT token management with refresh
- Secure credential handling (never logged or exposed)
- Clear error messages for authentication failures
✅ User Story 2: Market Data Queries
- Get current prices for symbols
- Retrieve order book depth
- Query recent trades
- Proper error handling with meaningful messages
- All responses deserialized into strongly-typed C# models
✅ User Story 3: Order Management
- Place, modify, and cancel orders
- Query open orders and order history
- Manage positions (close, partial close)
- Query trade executions
- Bracket orders with stop-loss and take-profit
✅ User Story 4: Real-Time Streaming Data
- WebSocket streaming via two SignalR hubs (Market Hub + User Hub)
- Subscribe to real-time price updates, order book depth, and trade executions
- Subscribe to real-time order status updates per account
- Automatic reconnection with exponential backoff (1s initial, 5s max), restoring subscriptions before reporting Connected
- Connection status monitoring via events
- Thread-safe, high-throughput (1000+ events/second)
Installation
Add the package reference to your project:
dotnet add package MarqSpec.Client.ProjectX
Configuration
Option 1: Environment Variables (Recommended for Production)
Set the following environment variables:
PROJECTX_API_KEY=your-api-key
PROJECTX_API_SECRET=your-api-secret
Option 2: appsettings.json
{
"ProjectX": {
"ApiKey": "your-api-key",
"ApiSecret": "your-api-secret",
"BaseUrl": "https://api.topstepx.com",
"RetryOptions": {
"MaxRetries": 3,
"InitialDelay": "00:00:01",
"MaxDelay": "00:00:30"
}
}
}
Note: Environment variables take precedence over appsettings.json
Quick Start
1. Register Services
In your Program.cs or Startup.cs:
using MarqSpec.Client.ProjectX.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Add ProjectX API client
builder.Services.AddProjectXApiClient(builder.Configuration);
var app = builder.Build();
DI lifetimes:
IProjectXApiClientis registered as Scoped andIProjectXWebSocketClientas Singleton. InjectIProjectXWebSocketClientinto long-lived services only; avoid resolving it from a scoped context directly.
2. Use the Client
using MarqSpec.Client.ProjectX;
using MarqSpec.Client.ProjectX.Api.Models;
public class TradingService
{
private readonly IProjectXApiClient _apiClient;
private readonly ILogger<TradingService> _logger;
public TradingService(IProjectXApiClient apiClient, ILogger<TradingService> logger)
{
_apiClient = apiClient;
_logger = logger;
}
public async Task RunAsync(CancellationToken cancellationToken = default)
{
try
{
// Get trading accounts
var accounts = await _apiClient.GetAccountsAsync(onlyActiveAccounts: true, cancellationToken);
var account = accounts.First();
_logger.LogInformation("Account {Id}: {Name} (Balance: {Balance})",
account.Id, account.Name, account.Balance);
// Search for live contracts
var contracts = await _apiClient.SearchContractsAsync("NQ", live: true, cancellationToken);
var contract = contracts.First();
_logger.LogInformation("Contract {Id}: {Name} (Tick: {Size}/{Value})",
contract.Id, contract.Name, contract.TickSize, contract.TickValue);
// Place a limit order
var orderResponse = await _apiClient.PlaceOrderAsync(new PlaceOrderRequest
{
AccountId = account.Id,
ContractId = contract.Id,
Type = OrderType.Limit,
Side = OrderSide.Bid,
Size = 1,
LimitPrice = 18000m
}, cancellationToken);
_logger.LogInformation("Order placed: {OrderId}", orderResponse.OrderId);
// Query open orders
var openOrders = await _apiClient.GetOpenOrdersAsync(account.Id, cancellationToken);
_logger.LogInformation("Open orders: {Count}", openOrders.Count());
// Query open positions
var positions = await _apiClient.GetOpenPositionsAsync(account.Id, cancellationToken);
_logger.LogInformation("Open positions: {Count}", positions.Count());
}
catch (ProjectXApiException ex)
{
_logger.LogError(ex, "API error: {Message}", ex.Message);
}
catch (AuthenticationException ex)
{
_logger.LogError(ex, "Authentication failed: {Message}", ex.Message);
}
}
}
Real-Time Streaming
The client provides real-time market data and order updates via two SignalR WebSocket hubs:
- Market Hub — price quotes, order book depth, and trade executions
- User Hub — order status updates for your accounts
Updates are delivered through C# events using the Observer pattern.
Streaming Market Data
using MarqSpec.Client.ProjectX;
using MarqSpec.Client.ProjectX.WebSocket;
public class MarketDataService : IAsyncDisposable
{
private readonly IProjectXWebSocketClient _wsClient;
private readonly ILogger<MarketDataService> _logger;
public MarketDataService(IProjectXWebSocketClient wsClient, ILogger<MarketDataService> logger)
{
_wsClient = wsClient;
_logger = logger;
}
public async Task StartStreamingAsync(string contractId, CancellationToken cancellationToken = default)
{
// Monitor connection status changes
_wsClient.ConnectionStatusChanged += (sender, change) =>
{
_logger.LogInformation("Market Hub: {Previous} → {Current}",
change.PreviousState, change.CurrentState);
if (change.ErrorMessage is not null)
_logger.LogWarning("Connection error: {Error}", change.ErrorMessage);
};
// Register event handlers
_wsClient.PriceUpdateReceived += (sender, update) =>
{
_logger.LogInformation("{Symbol} Last={Last} Bid={Bid} Ask={Ask} Chg={Chg}% Vol={Vol}",
update.Symbol, update.LastPrice,
update.BestBid, update.BestAsk,
update.ChangePercent, update.Volume);
};
_wsClient.OrderBookUpdateReceived += (sender, update) =>
{
_logger.LogInformation("{Contract} Depth: {Type} {Price} x {Volume}",
update.ContractId, update.Type, update.Price, update.Volume);
};
_wsClient.TradeUpdateReceived += (sender, update) =>
{
_logger.LogInformation("{Contract} Trade: {Price} x {Volume} ({Type})",
update.ContractId, update.Price, update.Volume, update.Type);
};
// Connect and subscribe
await _wsClient.ConnectMarketHubAsync(cancellationToken);
await _wsClient.SubscribeToPriceUpdatesAsync(contractId, cancellationToken);
await _wsClient.SubscribeToOrderBookUpdatesAsync(contractId, cancellationToken);
await _wsClient.SubscribeToTradeUpdatesAsync(contractId, cancellationToken);
}
public async Task StopStreamingAsync(string contractId, CancellationToken cancellationToken = default)
{
await _wsClient.UnsubscribeFromPriceUpdatesAsync(contractId, cancellationToken);
await _wsClient.UnsubscribeFromOrderBookUpdatesAsync(contractId, cancellationToken);
await _wsClient.UnsubscribeFromTradeUpdatesAsync(contractId, cancellationToken);
await _wsClient.DisconnectMarketHubAsync(cancellationToken);
}
public async ValueTask DisposeAsync()
{
await _wsClient.DisposeAsync();
}
}
Streaming Order Updates
// Connect to the User Hub for order status updates
await wsClient.ConnectUserHubAsync(cancellationToken);
wsClient.OrderUpdateReceived += (sender, update) =>
{
logger.LogInformation("Order {Id} on account {Acct}: {Status} — {Side} {Size} @ {Price}",
update.OrderId, update.AccountId, update.Status,
update.Side, update.Size, update.AverageFillPrice);
if (update.RejectionReason is not null)
logger.LogWarning("Rejected: {Reason}", update.RejectionReason);
};
await wsClient.SubscribeToOrderUpdatesAsync(accountId, cancellationToken);
// Later: clean up
await wsClient.UnsubscribeFromOrderUpdatesAsync(accountId, cancellationToken);
await wsClient.DisconnectUserHubAsync(cancellationToken);
Connection States
The ConnectionState enum tracks the lifecycle of each hub connection:
| State | Description |
|---|---|
Disconnected |
Not connected |
Connecting |
Connection attempt in progress |
Connected |
Active and receiving data |
Reconnecting |
Automatically reconnecting after a disconnection |
Failed |
Connection failed (check ErrorMessage on the event) |
Monitor state transitions via the ConnectionStatusChanged event, which provides a ConnectionStatusChange object containing PreviousState, CurrentState, Timestamp, ErrorMessage, and Exception.
Auto-Reconnection
Automatic reconnection is enabled by default. When a connection drops, the client uses exponential backoff starting at 1 second up to a maximum of 5 seconds. During reconnection, the ConnectionStatusChanged event fires with ConnectionState.Reconnecting. SignalR automatic reconnect is a new connection id, so server-side subscriptions are gone: the client re-invokes every recorded SubscribeTo* against the new connection before reporting Connected. A failed restore raises MessageSendFailed and reports Failed, not Connected. Connect*HubAsync after Failed disposes the previous hub and does the same restore-before-Connected step; a restore failure there is thrown to the caller. MarketSubscriptions and UserSubscriptions are recorded subscribe intent — what the next connect or reconnect will try to restore — not proof the server is currently delivering. Trust ConnectionState.Connected after a successful restore, not the snapshot alone. Configure reconnection behavior in appsettings.json:
{
"ProjectX": {
"WebSocket": {
"AutoReconnect": true,
"InitialReconnectDelaySeconds": 1,
"MaxReconnectDelaySeconds": 5
}
}
}
API Reference
IProjectXApiClient
Accounts
| Method | Parameters | Returns | Description |
|---|---|---|---|
GetAccountsAsync |
bool onlyActiveAccounts = true |
IEnumerable<TradingAccount> |
Get trading accounts |
Contracts
| Method | Parameters | Returns | Description |
|---|---|---|---|
SearchContractsAsync |
string? searchText, bool live = true |
IEnumerable<Contract> |
Search contracts by text |
GetContractAsync |
string contractId, bool live |
Contract? |
Deprecated — live is ignored; the by-ID route has no such field. Use GetContractByIdAsync |
GetContractByIdAsync |
string contractId |
Contract? |
Direct contract lookup by ID |
GetAvailableContractsAsync |
bool live = true |
IEnumerable<Contract> |
List all available contracts |
Historical Data
| Method | Parameters | Returns | Description |
|---|---|---|---|
GetHistoricalBarsAsync |
string contractId, DateTime startTime, DateTime endTime, AggregateBarUnit unit, int unitNumber = 1, int limit = 1000, bool live = true, bool includePartialBar = false |
IEnumerable<AggregateBar> |
Retrieve historical OHLCV bars |
Orders
| Method | Parameters | Returns | Description |
|---|---|---|---|
PlaceOrderAsync |
PlaceOrderRequest request |
PlaceOrderResponse |
Place a new order |
ModifyOrderAsync |
ModifyOrderRequest request |
ModifyOrderResponse |
Modify an existing order |
CancelOrderAsync |
int accountId, long orderId |
CancelOrderResponse |
Cancel an existing order |
GetOrderAsync |
int accountId, long orderId, DateTime startTime, DateTime? endTime |
Order? |
Get a specific order within a window |
GetOrdersAsync |
int accountId, DateTime? startTime, DateTime? endTime |
IEnumerable<Order> |
Get orders in a time range |
GetOpenOrdersAsync |
int accountId |
IEnumerable<Order> |
Get all open/working orders |
The order search requires a window. The gateway's
/api/Order/searchschema marksstartTimestamprequired. Omitting it lets the gateway apply a window of its own, and an order outside that window comes back absent — which a caller cannot tell apart from "no such order". In a reconciliation path that reads as a live order was never placed.So
GetOrdersAsyncthrowsArgumentExceptionwhenstartTimeis null, and the two-argumentGetOrderAsync(accountId, orderId)is[Obsolete(error: true)]— replaced by the overload above. Neither substitutes a default window, because a window the client invents silently reproduces exactly the failure it is meant to prevent. Choose one that certainly contains the order; when reconciling a placement, that means starting before the placement was attempted.
GetOpenOrdersAsyncneeds no window —/api/Order/searchOpentakes only an account.
Note also that SearchOrderRequest.ContractId and .Status are not in the gateway's schema. They are
serialized and ignored, so they filter nothing; filter the returned collection instead.
Positions
| Method | Parameters | Returns | Description |
|---|---|---|---|
GetOpenPositionsAsync |
int accountId |
IEnumerable<Position> |
Get all open positions |
ClosePositionAsync |
int accountId, string contractId |
ClosePositionResponse |
Close a full position |
PartialClosePositionAsync |
int accountId, string contractId, int size |
PartialClosePositionResponse |
Partially close a position |
Trades
| Method | Parameters | Returns | Description |
|---|---|---|---|
GetTradesAsync |
int accountId, DateTime? startTime, DateTime? endTime |
IEnumerable<HalfTrade> |
Get trade executions |
Utility
| Method | Returns | Description |
|---|---|---|
PingAsync |
bool |
Check if the API is responsive |
Note: All methods accept an optional
CancellationTokenparameter (omitted from tables for brevity).
IProjectXWebSocketClient
Connection Management
| Method | Description |
|---|---|
ConnectMarketHubAsync(CancellationToken) |
Connect to the market data hub |
DisconnectMarketHubAsync(CancellationToken) |
Disconnect from the market data hub |
ConnectUserHubAsync(CancellationToken) |
Connect to the user data hub |
DisconnectUserHubAsync(CancellationToken) |
Disconnect from the user data hub |
Properties
| Property | Type | Description |
|---|---|---|
MarketHubState |
ConnectionState |
Current connection state of the market hub |
UserHubState |
ConnectionState |
Current connection state of the user hub |
MarketSubscriptions |
MarketHubSubscriptions |
Recorded market-hub subscribe intent the client will try to restore; not a liveness guarantee |
UserSubscriptions |
UserHubSubscriptions |
Recorded user-hub subscribe intent the client will try to restore; not a liveness guarantee |
Market Data Subscriptions
| Method | Parameters | Description |
|---|---|---|
SubscribeToPriceUpdatesAsync |
string contractId, CancellationToken |
Subscribe to real-time price quotes |
UnsubscribeFromPriceUpdatesAsync |
string contractId, CancellationToken |
Unsubscribe from price quotes |
SubscribeToOrderBookUpdatesAsync |
string contractId, CancellationToken |
Subscribe to order book depth updates |
UnsubscribeFromOrderBookUpdatesAsync |
string contractId, CancellationToken |
Unsubscribe from order book updates |
SubscribeToTradeUpdatesAsync |
string contractId, CancellationToken |
Subscribe to trade execution updates |
UnsubscribeFromTradeUpdatesAsync |
string contractId, CancellationToken |
Unsubscribe from trade updates |
User Data Subscriptions
| Method | Parameters | Description |
|---|---|---|
SubscribeToOrderUpdatesAsync |
int accountId, CancellationToken |
Subscribe to order status updates |
UnsubscribeFromOrderUpdatesAsync |
int accountId, CancellationToken |
Unsubscribe from order updates |
Events
| Event | Payload | Description |
|---|---|---|
ConnectionStatusChanged |
ConnectionStatusChange |
Fires on any connection state transition |
PriceUpdateReceived |
PriceUpdate |
Fires on each price quote |
OrderBookUpdateReceived |
OrderBookUpdate |
Fires on each order book snapshot |
TradeUpdateReceived |
TradeUpdate |
Fires on each trade execution |
OrderUpdateReceived |
OrderUpdate |
Fires on each order status change |
PriceUpdate
| Property | Type | Description |
|---|---|---|
ContractId |
string |
Contract the market hub bound this event to (e.g. "CON.F.US.EP.Z26"). Stamped from the hub argument, not the JSON payload |
Symbol |
string |
Product-root symbol ID (e.g. "F.US.EP") |
SymbolName |
string? |
Friendly symbol name |
LastPrice |
decimal |
Last traded price |
BestBid |
decimal |
Best bid price |
BestAsk |
decimal |
Best ask price |
Change |
decimal |
Price change since previous close |
ChangePercent |
decimal |
Percent change since previous close |
Open |
decimal |
Session opening price |
High |
decimal |
Session high price |
Low |
decimal |
Session low price |
Volume |
decimal |
Total volume traded this session |
LastUpdated |
DateTime |
Last updated timestamp |
Timestamp |
DateTime |
Quote timestamp |
OrderBookUpdate
A single DOM (depth-of-market) row from GatewayDepth, not a full book snapshot.
| Property | Type | Description |
|---|---|---|
ContractId |
string |
Contract the market hub bound this event to. Stamped from the hub argument; the payload has no symbol |
Timestamp |
DateTime |
Update timestamp |
Type |
DomType |
DOM entry type (Ask, Bid, BestAsk, …) |
Price |
decimal |
Price level |
Volume |
decimal |
Total volume at this price level |
CurrentVolume |
int |
Current (delta) volume at this price level |
TradeUpdate
| Property | Type | Description |
|---|---|---|
ContractId |
string |
Contract the market hub bound this event to. Stamped from the hub argument; SymbolId is the product root |
SymbolId |
string |
Product-root symbol ID (e.g. "F.US.EP") |
Price |
decimal |
Trade price |
Timestamp |
DateTime |
Trade timestamp |
Type |
TradeLogType? |
Buy (wire 0) or Sell (wire 1). null when the venue omitted or sent an unrecognised type |
Volume |
decimal |
Trade volume |
OrderUpdate
| Property | Type | Description |
|---|---|---|
OrderId |
long |
Order identifier |
AccountId |
int |
Account identifier |
ContractId |
string |
Contract identifier |
Status |
OrderStatus |
Current order status |
Type |
OrderType |
Order type (Market, Limit, Stop, etc.) |
Side |
OrderSide? |
Bid (wire 0) or Ask (wire 1). null when the venue omitted side |
Size |
int |
Total order size |
FilledQuantity |
int |
Quantity filled so far |
RemainingQuantity |
int |
Quantity remaining |
LimitPrice |
decimal? |
Limit price (if applicable) |
StopPrice |
decimal? |
Stop price (if applicable) |
AverageFillPrice |
decimal? |
Average fill price |
Timestamp |
DateTime |
Update timestamp |
Message |
string? |
Update reason or message |
RejectionReason |
string? |
Rejection reason (if rejected) |
ConnectionStatusChange
| Property | Type | Description |
|---|---|---|
PreviousState |
ConnectionState |
Connection state before the change |
CurrentState |
ConnectionState |
Connection state after the change |
Timestamp |
DateTime |
When the state change occurred |
ErrorMessage |
string? |
Error description (if error-related) |
Exception |
Exception? |
Exception that caused the change (if any) |
Enums
ConnectionState
| Value | Description |
|---|---|
Disconnected |
Not connected |
Connecting |
Connection attempt in progress |
Connected |
Active and receiving data |
Reconnecting |
Automatically reconnecting after a disconnection |
Failed |
Connection failed |
MarketHubSubscriptions / UserHubSubscriptions
Recorded subscribe intent, not live server state. After Failed the sets still list what the next Connect* or automatic reconnect will try to restore. MarketHubSubscriptions holds the contract-id sets for quotes, depth and trades. UserHubSubscriptions holds the account-updates flag and the account-id sets for orders, positions and trade notifications.
OrderStatus
| Value | Description |
|---|---|
Unknown (0) |
Unknown status |
Accepted (1) |
Order accepted by the system |
Pending (2) |
Order pending execution |
Triggered (3) |
Stop order has been triggered |
PartiallyFilled (4) |
Order partially filled |
Filled (5) |
Order completely filled |
Cancelled (6) |
Order cancelled |
Rejected (7) |
Order rejected |
Expired (8) |
Order expired |
OrderType
| Value | Description |
|---|---|
Unknown (0) |
Unknown order type |
Limit (1) |
Executes at a specific price or better |
Market (2) |
Executes immediately at the best available price |
StopLimit (3) |
Becomes a limit order when the stop price is reached |
Stop (4) |
Becomes a market order when the stop price is reached |
TrailingStop (5) |
Stop price trails the market by a specified amount |
JoinBid (6) |
Automatically adjusts to join the best bid |
JoinAsk (7) |
Automatically adjusts to join the best ask |
OrderSide
| Value | Description |
|---|---|
Bid (0) |
Buy order. This is the live wire value — do not treat 0 as "unset" |
Ask (1) |
Sell order |
Order.Side, HalfTrade.Side, OrderUpdate.Side and TradeNotification.Side are OrderSide?. null means the venue omitted side. PlaceOrderRequest.Side stays required.
TradeLogType
| Value | Description |
|---|---|
Buy (0) |
Buy-side print. This is the live wire value — do not treat 0 as "unknown" |
Sell (1) |
Sell-side print |
TradeUpdate.Type is TradeLogType?. null means the venue omitted type or sent a value that is neither 0 nor 1.
PositionType
| Value | Description |
|---|---|
Undefined (0) |
Position direction is undefined |
Long (1) |
Long (buy) position |
Short (2) |
Short (sell) position |
AggregateBarUnit
| Value | Description |
|---|---|
Unspecified (0) |
Unspecified unit |
Second (1) |
Second-based aggregation |
Minute (2) |
Minute-based aggregation |
Hour (3) |
Hour-based aggregation |
Day (4) |
Day-based aggregation |
Week (5) |
Week-based aggregation |
Month (6) |
Month-based aggregation |
REST API Models
TradingAccount
| Property | Type | Description |
|---|---|---|
Id |
int |
Unique account identifier |
Name |
string |
Account name |
Balance |
decimal |
Current account balance |
CanTrade |
bool |
Whether this account is allowed to trade |
IsVisible |
bool |
Whether this account is visible |
Simulated |
bool |
Whether this is a simulated account |
Contract
| Property | Type | Description |
|---|---|---|
Id |
string |
Unique contract identifier |
Name |
string |
Contract name |
Description |
string |
Contract description |
TickSize |
decimal |
Minimum price increment |
TickValue |
decimal |
Monetary value of one tick |
ActiveContract |
bool |
Whether this is an active contract |
SymbolId |
string |
Symbol identifier |
Order
| Property | Type | Description |
|---|---|---|
Id |
long |
Unique order identifier |
AccountId |
int |
Account that owns this order |
ContractId |
string |
Contract identifier |
SymbolId |
string |
Symbol identifier |
CreationTimestamp |
DateTime |
When the order was created |
UpdateTimestamp |
DateTime? |
When the order was last updated |
Status |
OrderStatus |
Current order status |
Type |
OrderType |
Order type |
Side |
OrderSide? |
Bid (wire 0) or Ask (wire 1). null when the venue omitted side |
Size |
int |
Order quantity |
LimitPrice |
decimal? |
Limit price (for limit orders) |
StopPrice |
decimal? |
Stop price (for stop orders) |
FillVolume |
int |
Number of contracts filled |
FilledPrice |
decimal? |
Average fill price |
CustomTag |
string? |
Custom tag for the order |
Position
| Property | Type | Description |
|---|---|---|
Id |
int |
Unique position identifier |
AccountId |
int |
Account that owns this position |
ContractId |
string |
Contract identifier |
ContractDisplayName |
string? |
Human-readable contract name |
CreationTimestamp |
DateTime |
When this position was created |
Type |
PositionType |
Position direction (Long/Short) |
Size |
int |
Number of contracts |
AveragePrice |
decimal |
Volume-weighted average entry price |
HalfTrade
| Property | Type | Description |
|---|---|---|
Id |
long |
Unique trade identifier |
AccountId |
int |
Account that executed this trade |
ContractId |
string |
Contract identifier |
CreationTimestamp |
DateTime |
When the trade was executed |
Price |
decimal |
Execution price |
ProfitAndLoss |
decimal? |
Realized P&L for this trade leg |
Fees |
decimal |
Fees charged |
Side |
OrderSide? |
Bid (buy) or Ask (sell). null when the venue omitted side |
Size |
int |
Number of contracts traded |
Voided |
bool |
Whether this trade has been voided |
OrderId |
long |
ID of the order that generated this trade |
AggregateBar
| Property | Type | Description |
|---|---|---|
Timestamp |
DateTime |
Bar timestamp |
Open |
decimal |
Opening price |
High |
decimal |
Highest price during the period |
Low |
decimal |
Lowest price during the period |
Close |
decimal |
Closing price |
Volume |
long |
Trading volume |
PlaceOrderRequest
| Property | Type | Required | Description |
|---|---|---|---|
AccountId |
int |
Yes | Account identifier |
ContractId |
string |
Yes | Contract to trade |
Type |
OrderType |
Yes | Order type |
Side |
OrderSide |
Yes | Order side |
Size |
int |
Yes | Number of contracts |
LimitPrice |
decimal? |
No | Limit price (required for Limit/StopLimit orders) |
StopPrice |
decimal? |
No | Stop price (required for Stop/StopLimit orders) |
TrailPrice |
decimal? |
No | Trail amount (for TrailingStop orders) |
CustomTag |
string? |
No | Custom tag for order tracking |
StopLossBracket |
PlaceOrderBracket? |
No | Stop-loss bracket configuration |
TakeProfitBracket |
PlaceOrderBracket? |
No | Take-profit bracket configuration |
PlaceOrderBracket
| Property | Type | Description |
|---|---|---|
Ticks |
int |
Number of ticks from the entry price |
Type |
OrderType |
Bracket order type |
Error Handling
The client provides two exception types:
AuthenticationException
Thrown when authentication fails. Common causes:
- Invalid API key or secret
- Network connectivity issues
- API service unavailable
ProjectXApiException
Thrown when API requests fail. Includes:
- HTTP status code (if available)
- Detailed error message
- Original exception as inner exception
Features
Automatic Token Management
- Tokens are cached and automatically refreshed before expiration
- Thread-safe token acquisition
- 1-minute buffer before token expiration
Retry Policy with Polly
- Automatic retry for transient failures:
HttpRequestException, HTTP 429 (Too Many Requests), and HTTP 500+ server errors - Exponential backoff strategy (configurable initial and max delay)
- Retry-After header is honored on 429 responses (both delta-seconds and HTTP-date formats)
- Configurable retry attempts and delays
Logging
- Uses
ILogger<T>from Microsoft.Extensions.Logging - Compatible with any logging provider (Serilog, NLog, etc.)
- Structured logging with proper log levels
- Credentials are never logged
Thread Safety
- All methods are thread-safe
- Supports concurrent API calls
- Safe for use in multi-threaded applications
Configuration Options
Every option below is read by a code path. Two are listed as inert on purpose and say why; nothing else here is decorative.
All keys sit under the ProjectX section.
| Option | Type | Default | Description |
|---|---|---|---|
ApiKey |
string | required | Your ProjectX API key |
ApiSecret |
string | required | Your ProjectX API secret |
BaseUrl |
string | https://api.topstepx.com |
Base URL for the REST API |
RetryOptions.MaxRetries |
int | 3 | Retry attempts on a transient fault. Never applies to POST /api/Order/place |
RetryOptions.InitialDelay |
TimeSpan | 00:00:01 |
First backoff delay |
RetryOptions.MaxDelay |
TimeSpan | 00:00:30 |
Backoff ceiling |
WebSocket.UserHubUrl |
string | https://rtc.topstepx.com/hubs/user |
User hub URL |
WebSocket.MarketHubUrl |
string | https://rtc.topstepx.com/hubs/market |
Market hub URL |
WebSocket.AutoReconnect |
bool | true |
false stops reconnection entirely |
WebSocket.InitialReconnectDelaySeconds |
int | 1 | First reconnect backoff |
WebSocket.MaxReconnectDelaySeconds |
int | 5 | Reconnect backoff ceiling |
WebSocket.HandshakeTimeoutSeconds |
int | 15 | SignalR handshake timeout |
WebSocket.KeepAliveIntervalSeconds |
int | 15 | Keep-alive ping interval |
WebSocket.ServerTimeoutSeconds |
int | 30 | Server timeout before the connection is considered dead |
WebSocket.MaxBufferSize |
long | 1048576 | Transport and application buffer ceiling, in bytes |
Pointing at a different venue
The hub URLs have two accepted spellings, and both work:
{
"ProjectX": {
"WebSocket": {
"UserHubUrl": "https://rtc.example.com/hubs/user", // preferred
"MarketHubUrl": "https://rtc.example.com/hubs/market"
},
"WebSocketUserHubUrl": "https://rtc.example.com/hubs/user", // also honoured
"WebSocketMarketHubUrl": "https://rtc.example.com/hubs/market"
}
}
The nested WebSocket: form wins when both are set. Through 2.0.0 only the nested form was read, while this
table documented only the outer one — so a consumer following these docs to reach a simulation venue edited a
key that did nothing and stayed connected to production. Both resolve now.
Options that intentionally do nothing
| Option | Why |
|---|---|
ValidateSslCertificates |
TLS validation is always on and cannot be disabled. Honouring false would add a supported way to skip certificate validation against a live trading venue. Trust a self-signed endpoint at the host instead. |
WebSocket.UseMessagePack |
The hubs always speak JSON. MessagePack needs a protocol package, and this library's dependency surface is part of its public contract. |
Both are marked [Obsolete] and produce a compiler warning; neither has been removed, so nothing breaks.
Requirements
- .NET 8.0 or .NET 10.0 — the package multi-targets both, and both are first-class
- Valid ProjectX API credentials
License
MIT
Support
For issues and questions, please contact the development team.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.SignalR.Client (>= 10.0.11)
- Microsoft.Extensions.Configuration (>= 10.0.11)
- Microsoft.Extensions.Http (>= 10.0.11)
- Microsoft.Extensions.Http.Resilience (>= 10.9.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Options (>= 10.0.11)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.11)
- Polly.Core (>= 8.7.0)
- Refit (>= 15.2.0)
- Refit.HttpClientFactory (>= 15.2.0)
-
net8.0
- Microsoft.AspNetCore.SignalR.Client (>= 10.0.11)
- Microsoft.Extensions.Configuration (>= 10.0.11)
- Microsoft.Extensions.Http (>= 10.0.11)
- Microsoft.Extensions.Http.Resilience (>= 10.9.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Options (>= 10.0.11)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.11)
- Polly.Core (>= 8.7.0)
- Refit (>= 15.2.0)
- Refit.HttpClientFactory (>= 15.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.