Skip to content
Open
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
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ When you run this script, PyCharting will:

Once you have your OHLC series, you pass additional series to `plot` in two different ways:

<!-- Not executed: a fragment referencing the indicator functions and series you supply. -->
```python +RHIZA_SKIP
overlays = {
"SMA_50": sma(close, 50), # rendered on top of price
Expand Down Expand Up @@ -104,6 +105,7 @@ plot(

Each subplot value can be a plain array (line), a dict with options, or a list of dicts for multi-series panels:

<!-- Not executed: a fragment referencing the indicator arrays you supply. -->
```python +RHIZA_SKIP
subplots = {
# Simple line (default)
Expand Down Expand Up @@ -136,6 +138,7 @@ Supported series types: `"line"` (default), `"bar"`, `"scatter"`. Each entry acc

You can overlay buy/sell arrows on the price chart by passing a `trades` array aligned with your index. Values: `1` (buy), `-1` (sell), `0` (no trade).

<!-- Not executed: references your own index/OHLC series, and plot() starts a server and opens a browser. -->
```python +RHIZA_SKIP
import numpy as np

Expand Down Expand Up @@ -171,6 +174,7 @@ The public API is intentionally small and focused. All functions are available f

### `plot`

<!-- Not executed: an annotated signature for reference, not valid Python at a call site. -->
```python +RHIZA_SKIP
from typing import Dict, Any, Optional, Union

Expand Down Expand Up @@ -216,23 +220,35 @@ The returned dict includes:

### `stop_server`

```python +RHIZA_SKIP
```python
from pycharting import stop_server

stop_server()
```

With no server running, it says so and does nothing:

```result
ⓘ No active server to stop
```

Stops the active chart server if it is running. This is useful in long‑running processes and demos to clean up after you are done exploring charts.

### `get_server_status`

```python +RHIZA_SKIP
```python
from pycharting import get_server_status

status = get_server_status()
print(status)
```

Before any chart has been plotted:

```result
{'running': False, 'server_info': None, 'active_sessions': 0}
```

Returns a small dict with:

- `running`: whether the server is alive,
Expand Down
38 changes: 18 additions & 20 deletions src/pycharting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,24 @@
- **Easy to Use:** Simple Python API similar to matplotlib or plotly.

Usage:
The main entry point is the `plot` function.

```python
from pycharting import plot, stop_server
import numpy as np

# Prepare your data (numpy arrays or pandas Series)
index = np.arange(100)
open_data = np.random.rand(100) + 100
high_data = open_data + 1
low_data = open_data - 1
close_data = open_data + 0.5

# Create and open the chart
plot(index, open_data, high_data, low_data, close_data)

# ... keep the script running if needed ...
# input("Press Enter to stop...")
# stop_server()
```
The public surface is three functions, re-exported at the top level:

>>> import pycharting
>>> sorted(pycharting.__all__)
['__version__', 'get_server_status', 'plot', 'stop_server']

The main entry point is `plot`. It starts a local server and opens a
browser, so the example below is illustrative rather than executed here:

>>> import numpy as np
>>> from pycharting import plot, stop_server
>>> index = np.arange(100)
>>> open_data = np.random.rand(100) + 100
>>> high_data = open_data + 1
>>> low_data = open_data - 1
>>> close_data = open_data + 0.5
>>> plot(index, open_data, high_data, low_data, close_data) # doctest: +SKIP
>>> stop_server() # doctest: +SKIP

Exports:
- `plot`: Main function to create and display charts.
Expand Down
79 changes: 45 additions & 34 deletions src/pycharting/api/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,29 +137,33 @@ def plot(
- `server_running`: Boolean indicating if the server is active.

Example:
```python
import numpy as np
from pycharting import plot

# 1. Prepare Data
n = 1000
index = np.arange(n)
close = np.cumsum(np.random.randn(n)) + 100

# 2. Simple Line Chart
plot(index, close=close)

# 3. Candlestick Chart
open_p = close + np.random.randn(n) * 0.5
high = np.maximum(open_p, close) + np.abs(np.random.randn(n))
low = np.minimum(open_p, close) - np.abs(np.random.randn(n))

plot(
index, open_p, high, low, close,
overlays={"SMA 20": sma},
session_id="my-analysis"
)
```
Every call below starts a local server and opens a browser, so they are
shown rather than executed. The data preparation is real, though:

>>> import numpy as np
>>> from pycharting import plot
>>> n = 1000
>>> index = np.arange(n)
>>> close = np.cumsum(np.random.randn(n)) + 100
>>> len(close)
1000

A single series renders as a line chart:

>>> plot(index, close=close) # doctest: +SKIP

Supplying open/high/low as well renders candlesticks:

>>> open_p = close + np.random.randn(n) * 0.5
>>> high = np.maximum(open_p, close) + np.abs(np.random.randn(n))
>>> low = np.minimum(open_p, close) - np.abs(np.random.randn(n))
>>> bool(np.all(high >= np.maximum(open_p, close)))
True
>>> plot(
... index, open_p, high, low, close,
... overlays={"SMA 20": close},
... session_id="my-analysis",
... ) # doctest: +SKIP
"""
global _active_server

Expand Down Expand Up @@ -323,12 +327,11 @@ def stop_server() -> None:
If no server is running, this function does nothing and prints a message.

Example:
```python
from pycharting import stop_server
Calling it when nothing is running is safe and simply reports so:

# ... after done with analysis ...
stop_server()
```
>>> from pycharting import stop_server
>>> stop_server()
ⓘ No active server to stop
"""
global _active_server

Expand All @@ -352,13 +355,21 @@ def get_server_status() -> ServerStatus:
- `active_sessions`: Count of currently loaded datasets/sessions.

Example:
```python
from pycharting import get_server_status
With no server started, the reported status is inert:

>>> from pycharting import get_server_status
>>> status = get_server_status()
>>> sorted(status)
['active_sessions', 'running', 'server_info']
>>> status["running"]
False
>>> status["server_info"] is None
True

Once a server is up, ``server_info`` carries its host and port:

status = get_server_status()
if status['running']:
print(f"Server running at {status['server_info']['url']}")
```
>>> if status["running"]: # doctest: +SKIP
... print(f"Server running at {status['server_info']['url']}")
"""
global _active_server

Expand Down
25 changes: 13 additions & 12 deletions src/pycharting/core/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,17 @@ def find_free_port(start_port: int | None = None, end_port: int | None = None) -
RuntimeError: If no free port can be found in the requested range.

Example:
```python
# Let the OS pick a guaranteed-free ephemeral port.
port = find_free_port()
Let the OS pick a guaranteed-free ephemeral port:

>>> port = find_free_port()
>>> port > 0
True

Scan a preferred range instead:

# Scan a preferred range instead.
port = find_free_port(8000, 8010)
```
>>> port = find_free_port(8000, 8010)
>>> 8000 <= port < 8010
True
Comment on lines +77 to +81
"""
# No range requested: let the OS hand out a guaranteed-unique ephemeral port.
if start_port is None:
Expand Down Expand Up @@ -253,13 +257,10 @@ def run_server(
None: This function blocks until the server stops.

Example:
```python
# Run server on localhost, finding a free port automatically
run_server()
These block until the server stops, so they are not executed here.

# Run on a specific port
run_server(port=8080)
```
>>> run_server() # doctest: +SKIP
>>> run_server(port=8080) # doctest: +SKIP
"""
# Determine port
if port is None:
Expand Down
69 changes: 67 additions & 2 deletions src/pycharting/data/ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@ def validate_input(

Raises:
DataValidationError: If any validation check fails.

Example:
A single series is mapped to ``close`` and rendered as a line chart,
leaving the other price fields unset:

>>> import numpy as np
>>> result = validate_input(np.arange(3), close=np.array([10.0, 11.0, 12.0]))
>>> result["close"].tolist()
[10.0, 11.0, 12.0]
>>> result["open"] is None
True

With open and close supplied, high and low are filled in from them:

>>> result = validate_input(
... np.arange(3),
... open=np.array([10.0, 11.0, 12.0]),
... close=np.array([11.0, 10.0, 13.0]),
... )
>>> result["high"].tolist()
[11.0, 11.0, 13.0]
>>> result["low"].tolist()
[10.0, 10.0, 12.0]

A series whose length disagrees with the index is rejected:

>>> validate_input(np.arange(3), close=np.array([1.0, 2.0]))
Traceback (most recent call last):
...
pycharting.data.ingestion.DataValidationError: Close length (2) does not match index length (3)
"""
# Convert index to numpy array if needed
if isinstance(index, (pd.Index, pd.Series)):
Expand Down Expand Up @@ -234,7 +264,19 @@ def to_array(data: pd.Series | np.ndarray | list | None, name: str) -> np.ndarra


class DataManager:
"""High-performance data container and manager."""
"""High-performance data container and manager.

Validates and normalizes the supplied series once on construction, then
serves arbitrary slices of them cheaply — one slice per viewport move.

Example:
>>> import numpy as np
>>> dm = DataManager(np.arange(5), close=np.arange(5, dtype=float) * 10)
>>> dm.length
5
>>> dm.close.tolist()
[0.0, 10.0, 20.0, 30.0, 40.0]
"""

def __init__(
self,
Expand Down Expand Up @@ -332,7 +374,30 @@ def get_chunk(
start_index: int | None = None,
end_index: int | None = None,
) -> dict[str, Any]:
"""Return a JSON-serializable slice of the data between ``start_index`` and ``end_index``."""
"""Return a JSON-serializable slice of the data between ``start_index`` and ``end_index``.

Indices are clamped to the available range, so an over-wide request
returns what exists rather than raising.

Example:
>>> import numpy as np
>>> dm = DataManager(np.arange(5), close=np.arange(5, dtype=float))
>>> chunk = dm.get_chunk(1, 3)
>>> chunk["index"]
[1, 2]
>>> chunk["close"]
[1.0, 2.0]

Omitting both bounds returns the whole series:

>>> len(dm.get_chunk()["index"])
5

An end index past the last bar is clamped:

>>> dm.get_chunk(3, 99)["close"]
[3.0, 4.0]
"""
# Handle default values
if start_index is None:
start_index = 0
Expand Down