Metadata-Version: 2.4
Name: agualphacn
Version: 0.1.0
Summary: Python client for AGuAlpha Investment Platform API
Home-page: https://github.com/agualpha/agualpha-python
Author: AGuAlpha
Author-email: contact@agualpha.com
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Office/Business :: Financial
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Provides-Extra: async
Requires-Dist: aiohttp>=3.8.0; extra == "async"
Provides-Extra: pandas
Requires-Dist: pandas>=1.5.0; extra == "pandas"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: black>=22.0.0; extra == "dev"
Requires-Dist: mypy>=0.950; extra == "dev"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# AGuAlpha Python Client

Official Python library for accessing AGuAlpha Investment Platform data.

## Installation

```bash
pip install agualphacn
```

For async support:
```bash
pip install agualphacn[async]
```

For pandas integration:
```bash
pip install agualphacn[pandas]
```

## Quick Start

### Synchronous Usage

```python
from agualphacn import AGuAlphaClient

# Initialize client
client = AGuAlphaClient(api_key="sk-your-api-key-here")

# Get positions data
positions = client.get_positions()
print(f"Total positions: {positions.total}")

for position in positions.data:
    print(f"Date: {position.date}, Outstanding: {position.outstanding}")

# Get ideas data
ideas = client.get_ideas(status="active")
print(f"Total active ideas: {ideas.total}")

for idea in ideas.data:
    print(f"Symbol: {idea.ticker_symbol}, Status: {idea.status}")

# Close the connection
client.close()
```

### Using Context Manager

```python
from agualphacn import AGuAlphaClient

with AGuAlphaClient(api_key="sk-your-api-key-here") as client:
    positions = client.get_positions(
        start_date="2024-01-01",
        end_date="2024-12-31"
    )
    print(f"Found {positions.total} positions")
```

### Asynchronous Usage

```python
import asyncio
from agualphacn import AGuAlphaAsyncClient

async def main():
    async with AGuAlphaAsyncClient(api_key="sk-your-api-key-here") as client:
        # Fetch data concurrently
        positions, ideas = await asyncio.gather(
            client.get_positions(),
            client.get_ideas(status="active")
        )

        print(f"Positions: {positions.total}, Ideas: {ideas.total}")

asyncio.run(main())
```

### Pandas Integration

```python
from agualphacn import AGuAlphaClient
from agualphacn.utils import positions_to_dataframe, export_to_csv

client = AGuAlphaClient(api_key="sk-your-api-key-here")

# Get positions and convert to DataFrame
positions_response = client.get_positions()
df = positions_to_dataframe(positions_response.data)

# Export to CSV
export_to_csv(positions_response.data, "positions.csv")
```

### CSI Weights

Read `csi_weights` rows from the `prices` database filtered by index.
The `index` parameter is **required** and must be one of these semantic names:

| Value      | Index                          |
|------------|--------------------------------|
| `CSI300`   | CSI 300                        |
| `CSI500`   | CSI 500                        |
| `CSI1000`  | CSI 1000                       |
| `CSI2000`  | CSI 2000                       |

Numeric codes (e.g. `000300`) are **rejected** with HTTP 400.

Three mutually-exclusive date filters are supported:

```python
from agualphacn import AGuAlphaClient

with AGuAlphaClient(api_key="sk-your-api-key-here") as client:
    # 1) Single day
    df = client.get_csi_weights_dataframe(index="CSI300", date="2026-07-15")

    # 2) Inclusive range
    df = client.get_csi_weights_dataframe(
        index="CSI300",
        start_date="2026-07-01",
        end_date="2026-07-15",
    )

    # 3) Rolling window — last 30 days up to today
    df = client.get_csi_weights_dataframe(index="CSI300", days=30)

    print(df.head())
    print(f"Shape: {df.shape}")
```

If you don't need pandas, drop the `_dataframe` suffix to get a list of dicts:

```python
with AGuAlphaClient(api_key="sk-your-api-key-here") as client:
    rows = client.get_csi_weights(index="CSI300", days=30)
    print(f"Total records: {len(rows)}")
```

### Adjusted Close Prices

Read `adj_close` rows from the `prices` database. `membership` and a date
filter are **both required**. The membership selects a universe of tickers;
choose from:

| Value      | Universe                                                  |
|------------|-----------------------------------------------------------|
| `csi300`   | CSI 300 constituents                                      |
| `csi500`   | CSI 500 constituents                                      |
| `csi1000`  | CSI 1000 constituents                                     |
| `csi2000`  | CSI 2000 constituents                                     |
| `csi800`   | CSI 800 universe                                          |
| `csi1800`  | CSI 1800 universe                                         |
| `csi2800`  | CSI 2800 universe                                         |
| `x-csi`    | Cross-listed / uncategorized (NULL membership)            |
| `a-shares` | All A-shares (tickers ending `.SZ`, `.SH`, `.BJ`)         |
| `hk`       | Hong Kong listed (tickers ending `.HK`)                   |
| `etf`      | ETFs                                                      |

```python
from agualphacn import AGuAlphaClient

with AGuAlphaClient(api_key="sk-your-api-key-here") as client:
    # Single day for CSI 300 constituents
    df = client.get_adj_close_dataframe(membership="csi300", date="2026-07-15")

    # Inclusive range for HK tickers
    df = client.get_adj_close_dataframe(
        membership="hk",
        start_date="2026-07-01",
        end_date="2026-07-15",
    )
    print(df.head())
```

### P-CIES (HK / CN)

Read `p_cies` rows from the `prices` database filtered by zone. `zone` is
**required** and must be exactly `"hk"` or `"cn"`.

```python
from agualphacn import AGuAlphaClient

with AGuAlphaClient(api_key="sk-your-api-key-here") as client:
    df_hk = client.get_p_cies_dataframe(zone="hk")
    df_cn = client.get_p_cies_dataframe(zone="cn")
```

### SP Rolling Index (Commodities)

Read commodity rolling-index data from the `sp_rolling_index` table (prices
database). At least **one** filter is required: a product and/or a date filter.

```python
from agualphacn import AGuAlphaClient

with AGuAlphaClient(api_key="sk-your-api-key-here") as client:
    # List available product codes first (e.g. ["CU", "RB", ...])
    products = client.get_sp_rolling_index_products()

    # All rows for one product
    rows = client.get_sp_rolling_index(product="CU")

    # Multiple products at once (no count limit)
    rows = client.get_sp_rolling_index(products=["CU", "RB"])

    # All products in a date range (no product = all commodities)
    rows = client.get_sp_rolling_index(start_date="2025-06-01", end_date="2025-06-30")

    # Product + date range (intersection)
    df = client.get_sp_rolling_index_dataframe(
        product="CU", start_date="2025-06-01", end_date="2025-06-30",
    )

    # Single day
    rows = client.get_sp_rolling_index(date="2025-06-15")
```

> `date` and `start_date`/`end_date` are mutually exclusive. With no product
> and no date filter, the server returns `400 Bad Request`.

### Limit Events

Read `limit_evts` rows (daily limit-up / limit-down events) from the `prices`
database. A **date filter is required** — either a single day or an inclusive
range.

```python
from agualphacn import AGuAlphaClient

with AGuAlphaClient(api_key="sk-your-api-key-here") as client:
    # Single day
    rows = client.get_limit_evts(date="2024-06-15")

    # Inclusive range as a pandas DataFrame
    df = client.get_limit_evts_dataframe(
        start_date="2024-06-01",
        end_date="2024-06-30",
    )
    print(df.head())
```

> `date` and `start_date`/`end_date` are mutually exclusive. With no date
> filter, the server returns `400 Bad Request`.

## API Reference

### AGuAlphaClient

#### `__init__(api_key: str, base_url: str = "https://www.agualpha.cn/api")`
Initialize the client with your API key.

#### `get_positions(start_date: Optional[str] = None, end_date: Optional[str] = None) -> PositionsResponse`
Get position data from subscribed analysts.

**Parameters:**
- `start_date` (str): Filter by start date (YYYY-MM-DD format)
- `end_date` (str): Filter by end date (YYYY-MM-DD format)

**Returns:** `PositionsResponse`

#### `get_ideas(status: Optional[str] = None, direction: Optional[str] = None) -> IdeasResponse`
Get trade ideas from subscribed analysts.

**Parameters:**
- `status` (str): Filter by status ("active", "closed")
- `direction` (str): Filter by direction ("long", "short")

**Returns:** `IdeasResponse`

#### `get_csi_weights(index: str, date=None, start_date=None, end_date=None, days=None, page=None, page_size=None) -> list`
Get CSI weights data filtered by index. `index` is **required** and must be one of
`CSI1000`, `CSI2000`, `CSI300`, `CSI500`. At most one of the date filters may be
applied: `date` (single day), `start_date`+`end_date` (range), or `days` (rolling
window). Auto-paginates unless `page` is given.

**Returns:** `list` of dicts, each representing a row from the `csi_weights` table.

#### `get_csi_weights_dataframe(index: str, date=None, start_date=None, end_date=None, days=None, page=None, page_size=None) -> pandas.DataFrame`
Same parameters as `get_csi_weights`. Returns a DataFrame. Requires the `pandas` extra (`pip install agualphacn[pandas]`).

**Returns:** `pandas.DataFrame`

#### `get_revision_fy2(date=None, start_date=None, end_date=None, days=None, ticker=None, type=None, page=None, page_size=None) -> list`
Get revision_fy2 data from the `cn_af` database. A date filter is **required** —
exactly one of `date` (single day), `start_date`+`end_date` (range), or `days`
(rolling window). Optional: `ticker` (e.g. `"000009.SZ"`) and `type`
(`"eps"`/`"np"`/`"sales"`, no value = all types). Auto-paginates unless `page` is given.

**Returns:** `list` of dicts, each representing a row from the `revision_fy2` table.

#### `get_revision_fy2_dataframe(date=None, start_date=None, end_date=None, days=None, ticker=None, type=None, page=None, page_size=None) -> pandas.DataFrame`
Same parameters as `get_revision_fy2`. A date filter is **required**. Requires the `pandas` extra (`pip install agualphacn[pandas]`).

**Returns:** `pandas.DataFrame`

#### `get_adj_close(membership: str, date=None, start_date=None, end_date=None, page=None, page_size=None) -> list`
Get adj_close data from the `prices` database. `membership` is **required**
(universe selector — see the table above); a date filter is **required** —
either `date` (single day) or `start_date`+`end_date` (inclusive range, either
bound optional). Auto-paginates unless `page` is given.

**Returns:** `list` of dicts, each representing a row from `adj_close`.

#### `get_adj_close_dataframe(membership: str, date=None, start_date=None, end_date=None, page=None, page_size=None) -> pandas.DataFrame`
Same parameters as `get_adj_close`. `membership` and a date filter are **required**. Requires the `pandas` extra (`pip install agualphacn[pandas]`).

**Returns:** `pandas.DataFrame`

#### `get_p_cies(zone: str, page=None, page_size=None) -> list`
Get p_cies data from the `prices` database. `zone` is **required** and must be
`"hk"` or `"cn"`. Auto-paginates unless `page` is given.

**Returns:** `list` of dicts, each representing a row from `p_cies`.

#### `get_p_cies_dataframe(zone: str, page=None, page_size=None) -> pandas.DataFrame`
Same parameters as `get_p_cies`. `zone` is **required**. Requires the `pandas` extra (`pip install agualphacn[pandas]`).

#### `get_sp_rolling_index(product=None, products=None, date=None, start_date=None, end_date=None, page=None, page_size=None) -> list`
Get commodity rolling-index rows from the `prices` database. At least **one**
of `product(s)` or a date filter is **required**. `date` and `start_date`/`end_date`
are mutually exclusive.

- `product` (single string) and `products` (list) are merged; you can pass either or both.
- With no `product(s)` and no date filter, the server returns `400`.

**Returns:** `list` of dicts, each representing a row from `sp_rolling_index`.

#### `get_sp_rolling_index_dataframe(product=None, products=None, date=None, start_date=None, end_date=None, page=None, page_size=None) -> pandas.DataFrame`
Same parameters as `get_sp_rolling_index`. Requires the `pandas` extra.

#### `get_sp_rolling_index_products() -> list`
Return all distinct product codes available in `sp_rolling_index`, sorted ascending.
Useful for discovering which values are valid for the `product(s)` parameter.

**Returns:** `pandas.DataFrame`

#### `get_limit_evts(date=None, start_date=None, end_date=None, page=None, page_size=None) -> list`
Get `limit_evts` rows (daily limit-up / limit-down events) from the `prices`
database. A date filter is **required**: either `date` (single day) or
`start_date`/`end_date` (inclusive range). `date` and `start_date`/`end_date`
are mutually exclusive.

**Returns:** `list` of dicts, each representing a row from `limit_evts`.

#### `get_limit_evts_dataframe(date=None, start_date=None, end_date=None, page=None, page_size=None) -> pandas.DataFrame`
Same parameters as `get_limit_evts`. Requires the `pandas` extra.

### Response Models

#### `PositionsResponse`
- `success` (bool): Request success status
- `total` (int): Total number of records
- `data` (List[Position]): List of position objects
- `error` (str, optional): Error message if failed

#### `IdeasResponse`
- `success` (bool): Request success status
- `total` (int): Total number of records
- `data` (List[Idea]): List of idea objects
- `error` (str, optional): Error message if failed

## Error Handling

```python
from agualphacn import AGuAlphaClient
from agualphacn.exceptions import APIError, AuthenticationError, RateLimitError

try:
    client = AGuAlphaClient(api_key="invalid-key")
    positions = client.get_positions()
except AuthenticationError:
    print("Invalid API key")
except RateLimitError as e:
    print(f"Too many requests: {e}")
except APIError as e:
    print(f"API error: {e}")
```

## Rate Limiting

The data API limits each API key to **4 requests per second** (sliding 1-second
window). When the limit is exceeded, the server returns HTTP `429 Too Many
Requests` with a `Retry-After` header indicating how many seconds to wait. The
SDK surfaces this as a `RateLimitError` (subclass of `APIError`), so you can
catch it and back off:

```python
import time
from agualphacn import AGuAlphaClient
from agualphacn.exceptions import RateLimitError

client = AGuAlphaClient(api_key="your-api-key")
while True:
    try:
        data = client.get_positions()
        break
    except RateLimitError as e:
        time.sleep(1)
```

## Pagination

The server caps each request at **2000 rows**. By default, the SDK
**auto-paginates** — calling `get_positions()`, `get_ideas()`,
`get_csi_weights(index=...)`, or `get_revision_fy2(date=...)` with no page
argument walks every page and returns the full result set in one call. Each
underlying page request counts against your 4 req/s limit, so large tables
cost multiple requests.

If you want a single page, pass `page` (1-indexed) and optionally `page_size`
(server caps at 2000):

```python
# Fetch only the first 500 rows of revision_fy2 in the last 30 days
rows = client.get_revision_fy2(days=30, page=1, page_size=500)
```

When paginating manually, the response object exposes `page`, `page_size`,
`total_pages`, and `has_more` so you can loop pages yourself:

```python
page = 1
all_rows = []
while True:
    resp = client.get_positions(page=page, page_size=1000)
    all_rows.extend(resp.data)
    if not resp.has_more:
        break
    page += 1
```


## Requirements

- Python 3.8+
- requests 2.28.0+

## License

MIT License
