Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions DEVELOPMENT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,21 @@ main

### Phase 2: Important Enhancements (Weeks 4-5)

#### 2.1 Account Configuration
#### 2.1 Account Configuration
**Branch**: `feature/account-config`
**Priority**: 🟡 High
**Estimated Time**: 1 day
**Actual Time**: < 1 day
**Completed**: 2025-01-15

**Tasks**:
- [ ] Update `trading/account.py` module
- [ ] Implement `get_configuration()` method
- [ ] Implement `update_configuration()` method
- [ ] Create `AccountConfigModel` dataclass
- [ ] Add PDT, trade confirmation settings
- [ ] Add comprehensive tests (5+ test cases)
- [ ] Update documentation
- [x] Update `trading/account.py` module
- [x] Implement `get_configuration()` method
- [x] Implement `update_configuration()` method
- [x] Create `AccountConfigModel` dataclass
- [x] Add PDT, trade confirmation, margin, and all configuration settings
- [x] Add comprehensive tests (14 unit tests, 8 integration tests)
- [x] Update documentation

**Acceptance Criteria**:
- Can read and update account configurations
Expand Down Expand Up @@ -288,12 +290,12 @@ main

## 📈 Progress Tracking

### Overall Progress: 🟦 30% Complete
### Overall Progress: 🟦 35% Complete

| Phase | Status | Progress | Estimated Completion |
|-------|--------|----------|---------------------|
| Phase 1: Critical Features | ✅ Complete | 100% | Week 1 |
| Phase 2: Important Enhancements | ⬜ Not Started | 0% | Week 5 |
| Phase 2: Important Enhancements | 🟦 In Progress | 33% | Week 2 |
| Phase 3: Performance & Quality | ⬜ Not Started | 0% | Week 7 |
| Phase 4: Advanced Features | ⬜ Not Started | 0% | Week 10 |

Expand Down
47 changes: 47 additions & 0 deletions src/py_alpaca_api/models/account_config_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from dataclasses import dataclass


@dataclass
class AccountConfigModel:
"""Model for account configuration settings.

Attributes:
dtbp_check: Day trade buying power check setting ("entry", "exit", "both")
fractional_trading: Whether fractional trading is enabled
max_margin_multiplier: Maximum margin multiplier allowed ("1", "2", "4")
no_shorting: Whether short selling is disabled
pdt_check: Pattern day trader check setting ("entry", "exit", "both")
ptp_no_exception_entry: Whether PTP no exception entry is enabled
suspend_trade: Whether trading is suspended
trade_confirm_email: Trade confirmation email setting ("all", "none")
"""

dtbp_check: str
fractional_trading: bool
max_margin_multiplier: str
no_shorting: bool
pdt_check: str
ptp_no_exception_entry: bool
suspend_trade: bool
trade_confirm_email: str


def account_config_class_from_dict(data: dict) -> AccountConfigModel:
"""Create AccountConfigModel from API response dictionary.

Args:
data: Dictionary containing account configuration data from API

Returns:
AccountConfigModel instance
"""
return AccountConfigModel(
dtbp_check=data.get("dtbp_check", "entry"),
fractional_trading=data.get("fractional_trading", False),
max_margin_multiplier=data.get("max_margin_multiplier", "1"),
no_shorting=data.get("no_shorting", False),
pdt_check=data.get("pdt_check", "entry"),
ptp_no_exception_entry=data.get("ptp_no_exception_entry", False),
suspend_trade=data.get("suspend_trade", False),
trade_confirm_email=data.get("trade_confirm_email", "all"),
)
111 changes: 111 additions & 0 deletions src/py_alpaca_api/trading/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
AccountActivityModel,
account_activity_class_from_dict,
)
from py_alpaca_api.models.account_config_model import (
AccountConfigModel,
account_config_class_from_dict,
)
from py_alpaca_api.models.account_model import AccountModel, account_class_from_dict


Expand Down Expand Up @@ -183,3 +187,110 @@ def portfolio_history(
# Ensure we always return a DataFrame
assert isinstance(portfolio_df, pd.DataFrame)
return portfolio_df

############################################
# Get Account Configuration
############################################
def get_configuration(self) -> AccountConfigModel:
"""Retrieves the current account configuration settings.

Returns:
AccountConfigModel: The current account configuration.

Raises:
APIRequestError: If the request to retrieve configuration fails.
"""
url = f"{self.base_url}/account/configurations"
http_response = Requests().request("GET", url, headers=self.headers)

if http_response.status_code != 200:
raise APIRequestError(
http_response.status_code,
f"Failed to retrieve account configuration: {http_response.status_code}",
)

response = json.loads(http_response.text)
return account_config_class_from_dict(response)

############################################
# Update Account Configuration
############################################
def update_configuration(
self,
dtbp_check: str | None = None,
fractional_trading: bool | None = None,
max_margin_multiplier: str | None = None,
no_shorting: bool | None = None,
pdt_check: str | None = None,
ptp_no_exception_entry: bool | None = None,
suspend_trade: bool | None = None,
trade_confirm_email: str | None = None,
) -> AccountConfigModel:
"""Updates the account configuration settings.

Args:
dtbp_check: Day trade buying power check ("entry", "exit", "both")
fractional_trading: Whether to enable fractional trading
max_margin_multiplier: Maximum margin multiplier ("1", "2", "4")
no_shorting: Whether to disable short selling
pdt_check: Pattern day trader check ("entry", "exit", "both")
ptp_no_exception_entry: Whether to enable PTP no exception entry
suspend_trade: Whether to suspend trading
trade_confirm_email: Trade confirmation emails ("all", "none")

Returns:
AccountConfigModel: The updated account configuration.

Raises:
APIRequestError: If the request to update configuration fails.
ValueError: If invalid parameter values are provided.
"""
# Validate parameters using a validation map
validations = {
"dtbp_check": (dtbp_check, ["entry", "exit", "both"]),
"pdt_check": (pdt_check, ["entry", "exit", "both"]),
"max_margin_multiplier": (max_margin_multiplier, ["1", "2", "4"]),
"trade_confirm_email": (trade_confirm_email, ["all", "none"]),
}

for param_name, (value, valid_values) in validations.items():
if value and value not in valid_values:
raise ValueError(
f"{param_name} must be one of: {', '.join(valid_values)}"
)

# Build request body with only provided parameters
body: dict[str, str | bool] = {}
if dtbp_check is not None:
body["dtbp_check"] = dtbp_check
if fractional_trading is not None:
body["fractional_trading"] = fractional_trading
if max_margin_multiplier is not None:
body["max_margin_multiplier"] = max_margin_multiplier
if no_shorting is not None:
body["no_shorting"] = no_shorting
if pdt_check is not None:
body["pdt_check"] = pdt_check
if ptp_no_exception_entry is not None:
body["ptp_no_exception_entry"] = ptp_no_exception_entry
if suspend_trade is not None:
body["suspend_trade"] = suspend_trade
if trade_confirm_email is not None:
body["trade_confirm_email"] = trade_confirm_email

if not body:
raise ValueError("At least one configuration parameter must be provided")

url = f"{self.base_url}/account/configurations"
http_response = Requests().request(
"PATCH", url, headers=self.headers, json=body
)

if http_response.status_code != 200:
raise APIRequestError(
http_response.status_code,
f"Failed to update account configuration: {http_response.status_code}",
)

response = json.loads(http_response.text)
return account_config_class_from_dict(response)
183 changes: 183 additions & 0 deletions tests/test_integration/test_account_config_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import os

import pytest

from py_alpaca_api import PyAlpacaAPI
from py_alpaca_api.models.account_config_model import AccountConfigModel


@pytest.mark.skipif(
not os.environ.get("ALPACA_API_KEY") or not os.environ.get("ALPACA_SECRET_KEY"),
reason="API credentials not set",
)
class TestAccountConfigIntegration:
@pytest.fixture
def alpaca(self):
return PyAlpacaAPI(
api_key=os.environ.get("ALPACA_API_KEY"),
api_secret=os.environ.get("ALPACA_SECRET_KEY"),
api_paper=True,
)

@pytest.fixture
def original_config(self, alpaca):
"""Get the original configuration to restore after tests."""
return alpaca.trading.account.get_configuration()

def test_get_configuration(self, alpaca):
config = alpaca.trading.account.get_configuration()

assert isinstance(config, AccountConfigModel)
# These fields should always be present
assert config.dtbp_check in ["entry", "exit", "both"]
assert isinstance(config.fractional_trading, bool)
assert config.max_margin_multiplier in ["1", "2", "4"]
assert isinstance(config.no_shorting, bool)
assert config.pdt_check in ["entry", "exit", "both"]
assert isinstance(config.ptp_no_exception_entry, bool)
assert isinstance(config.suspend_trade, bool)
assert config.trade_confirm_email in ["all", "none"]

def test_update_single_configuration_param(self, alpaca, original_config):
# Toggle trade confirmation email
new_setting = "none" if original_config.trade_confirm_email == "all" else "all"

updated_config = alpaca.trading.account.update_configuration(
trade_confirm_email=new_setting
)

assert isinstance(updated_config, AccountConfigModel)
assert updated_config.trade_confirm_email == new_setting

# Verify other settings remain unchanged
assert updated_config.dtbp_check == original_config.dtbp_check
assert updated_config.fractional_trading == original_config.fractional_trading
assert updated_config.no_shorting == original_config.no_shorting
assert updated_config.pdt_check == original_config.pdt_check
assert updated_config.suspend_trade == original_config.suspend_trade

# Restore original setting
alpaca.trading.account.update_configuration(
trade_confirm_email=original_config.trade_confirm_email
)

def test_update_multiple_configuration_params(self, alpaca, original_config):
# Toggle no_shorting and change pdt_check
new_no_shorting = not original_config.no_shorting
new_pdt_check = "exit" if original_config.pdt_check == "entry" else "entry"

updated_config = alpaca.trading.account.update_configuration(
no_shorting=new_no_shorting, pdt_check=new_pdt_check
)

assert isinstance(updated_config, AccountConfigModel)
assert updated_config.no_shorting == new_no_shorting
assert updated_config.pdt_check == new_pdt_check

# Verify other settings remain unchanged
assert updated_config.dtbp_check == original_config.dtbp_check
assert updated_config.fractional_trading == original_config.fractional_trading
assert (
updated_config.max_margin_multiplier
== original_config.max_margin_multiplier
)
assert updated_config.suspend_trade == original_config.suspend_trade
assert updated_config.trade_confirm_email == original_config.trade_confirm_email

# Restore original settings
alpaca.trading.account.update_configuration(
no_shorting=original_config.no_shorting,
pdt_check=original_config.pdt_check,
)

def test_update_margin_multiplier(self, alpaca, original_config):
# Test changing margin multiplier
current_multiplier = original_config.max_margin_multiplier
new_multiplier = "2" if current_multiplier != "2" else "4"

updated_config = alpaca.trading.account.update_configuration(
max_margin_multiplier=new_multiplier
)

assert updated_config.max_margin_multiplier == new_multiplier

# Restore original
alpaca.trading.account.update_configuration(
max_margin_multiplier=original_config.max_margin_multiplier
)

def test_update_dtbp_check(self, alpaca, original_config):
# Cycle through dtbp_check options
options = ["entry", "exit", "both"]
current = original_config.dtbp_check
new_value = options[(options.index(current) + 1) % 3]

updated_config = alpaca.trading.account.update_configuration(
dtbp_check=new_value
)

assert updated_config.dtbp_check == new_value

# Restore original
alpaca.trading.account.update_configuration(
dtbp_check=original_config.dtbp_check
)

def test_toggle_fractional_trading(self, alpaca, original_config):
# Toggle fractional trading
new_value = not original_config.fractional_trading

updated_config = alpaca.trading.account.update_configuration(
fractional_trading=new_value
)

assert updated_config.fractional_trading == new_value

# Restore original
alpaca.trading.account.update_configuration(
fractional_trading=original_config.fractional_trading
)

def test_configuration_persistence(self, alpaca, original_config):
# Update a configuration
new_email_setting = (
"none" if original_config.trade_confirm_email == "all" else "all"
)
alpaca.trading.account.update_configuration(
trade_confirm_email=new_email_setting
)

# Get configuration again to verify persistence
config = alpaca.trading.account.get_configuration()
assert config.trade_confirm_email == new_email_setting

# Restore original
alpaca.trading.account.update_configuration(
trade_confirm_email=original_config.trade_confirm_email
)

def test_invalid_parameter_handling(self, alpaca):
# Test that invalid parameters raise appropriate errors
with pytest.raises(ValueError):
alpaca.trading.account.update_configuration(dtbp_check="invalid")

with pytest.raises(ValueError):
alpaca.trading.account.update_configuration(pdt_check="invalid")

with pytest.raises(ValueError):
alpaca.trading.account.update_configuration(max_margin_multiplier="5")

with pytest.raises(ValueError):
alpaca.trading.account.update_configuration(trade_confirm_email="sometimes")

@pytest.mark.skip(reason="Suspend trade affects account functionality")
def test_suspend_trade_toggle(self, alpaca, original_config):
# This test is skipped as it would actually suspend trading
# Only run manually when testing this specific feature
updated_config = alpaca.trading.account.update_configuration(suspend_trade=True)
assert updated_config.suspend_trade is True

# Immediately restore
alpaca.trading.account.update_configuration(
suspend_trade=original_config.suspend_trade
)
Loading
Loading