Metadata-Version: 2.4
Name: pylightcharts
Version: 0.1.0
Summary: TradingView Lightweight Charts for Python (bridged, near-full API coverage)
Author: pylightcharts contributors
License: MIT
Project-URL: Homepage, https://github.com/your-org/pylightcharts
Keywords: financial-charting,charting,candlestick,tradingview,lightweight-charts
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas>=1.5
Requires-Dist: numpy>=1.23
Requires-Dist: pywebview>=5.0.5
Provides-Extra: qt
Requires-Dist: PySide6; extra == "qt"
Provides-Extra: jupyter
Requires-Dist: ipython; extra == "jupyter"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pillow; extra == "test"
Provides-Extra: e2e
Requires-Dist: playwright; extra == "e2e"
Requires-Dist: pillow; extra == "e2e"
Provides-Extra: docs
Requires-Dist: mkdocs-material; extra == "docs"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: pillow; extra == "dev"
Requires-Dist: playwright; extra == "dev"
Requires-Dist: mkdocs-material; extra == "dev"
Dynamic: license-file

# pylightcharts

Python bindings for [TradingView Lightweight Charts™](https://github.com/tradingview/lightweight-charts),
aiming at **near-full API coverage** with a pythonic interface.

> Status: development (P0/P1/P2 hardening — 17 indicators, 13 drawing tools, numeric axes, formatters, CI with JS + Python tests and docs).

## How it works

This is **not** a re-implementation of the charting engine in Python.
It embeds the official, battle-tested JavaScript renderer in a WebView
(pywebview / Qt WebEngine / wx / Jupyter / Streamlit) and drives it from Python.

```
Python API  ──►  JSON / JS bridge  ──►  Lib.Handler (TypeScript)  ──►  Lightweight Charts v5
```

Because the pixels and interactions are produced by the upstream library,
visual fidelity and performance match the JS version.

## Acknowledgements / License

- Python layer, GUI backends and drawing plugins are derived from
  [lightweight-charts-python](https://github.com/louisnw01/lightweight-charts-python) (MIT).
- The charting engine is
  [TradingView Lightweight Charts™](https://github.com/tradingview/lightweight-charts) (Apache-2.0),
  see `NOTICE`.

## Migrating from lightweight-charts-python
`pylightcharts` keeps every class, method and module-level name of the original
package (enforced by `tests/test_lwc_compat.py`), so existing code runs with a
one-line shim:

```python
from pylightcharts.compat import install_alias
install_alias()                     # before any `import lightweight_charts`

import lightweight_charts            # -> pylightcharts
lightweight_charts.widgets.QWebEngineView = MyPyQt6WebEngineView   # patching still works
```

Or change the imports directly:

```diff
- from lightweight_charts.util import Events, JSEmitter
- from lightweight_charts.widgets import QtChart
+ from pylightcharts.util import Events, JSEmitter
+ from pylightcharts.widgets import QtChart
```

## Qt integration

Inside a desktop application, `QtChart` embeds the chart in a `QWebEngineView`:

```python
from PyQt6.QtWidgets import QApplication
from pylightcharts.qt import prepare_qt

prepare_qt('PyQt6')          # pick the binding *before* QApplication
app = QApplication([])

from pylightcharts.widgets import QtChart
chart = QtChart(toolbox=True)
chart.set(df)
chart.get_webview().show()
app.exec()
```

`QtWebEngineWidgets` must be imported before the `QApplication` exists, which is
why `prepare_qt()` comes first; `$PYLIGHTCHARTS_QT` chooses the binding without it.
Full details - multi-chart `sync()`, screenshots, the Chromium network noise -
are in `docs/guide/qt.md`.

## Examples

`examples/` in the repository holds ten self-contained scripts, each with its own
data: loading OHLCV, live bars, tick updates, indicator lines, styling,
callbacks, built-in indicators and panes, headless PNG rendering, custom series
and the PyQt6 window. Start with `examples/1_setting_data/setting_data.py`.

## Development

```bash
# build the JS bridge + vendor the engine into pylightcharts/js/
cd jslib && npm install && npm run build

# install the python package in editable mode
cd .. && pip install -e .

# ...or use the requirement files (pyproject.toml stays the source of truth)
pip install -r requirements.txt        # runtime only
pip install -r requirements-dev.txt    # + tests, e2e, docs, build
```

### Testing

```bash
# unit tests: assert on the JS generated by the python bridge (no browser)
python -m pytest -m "not slow"

# e2e: run the built bundle in headless Chromium and check rendered pixels
python tests/e2e/smoke.py

# headless rendering tests (Playwright or a local Chrome/Edge)
python -m pytest tests/test_headless.py

# full stack: real pywebview window + real chart engine (needs a desktop session)
python tests/e2e/full_stack.py

# performance: JSON vs binary bulk data transfer
python tests/e2e/benchmark.py 100000
```

CI (`.github/workflows/ci.yml`) builds the bundle, checks the committed artifacts
are up to date, runs the unit tests on Linux/Windows/macOS, renders in headless
Chromium and verifies the wheel ships the JS assets.

### Packaging

```bash
cd jslib && npm run build      # regenerate pylightcharts/js/*
cd .. && python -m build       # sdist + wheel (JS assets are package-data)
twine upload dist/*
```

The built `pylightcharts/js/` files are committed on purpose so that
`pip install` works without Node. Always run `npm run build` before committing.

### Bridge rules

- Fire-and-forget calls go through `Window.invoke` (the transport appends
  `;undefined` so pywebview never serialises a live chart object).
- Methods that **return an object** must use `store_as=...` or, for primitives,
  `invoke_get`. Returning a live object through `invoke_get` would explode.
- Round-trips are serialised and correlated by request id; JS errors raise
  `pylightcharts.util.BridgeError` instead of returning `None`.
- Chart and series share method names (`apply_options`); they are deliberately
  kept apart (`series_options`, `data_points`) to avoid MRO shadowing.
- Upgrading lightweight-charts: `npm pack lightweight-charts@X`, migrate the
  breaking changes, `npm run build`, then run the e2e suite.

### The generic bridge

New features should not require touching TypeScript. Any method on any live
chart object can be called from Python:

```python
chart._invoke('addSeries', 'Area', 'my area', {'lineWidth': 2}, handle)
chart.win.invoke(f'{series.id}.series', 'applyOptions', {'lineWidth': 3})
```

Handles are either registered via `Lib.register(...)` or dotted window paths
(`window.abcdefgh.chart`). A `{'$ref': handle}` argument is resolved to the live
JS object, so object-taking APIs work too:

```python
series.attach_primitive('myPrimitive')   # -> attachPrimitive({"$ref": "myPrimitive"})
```

See `jslib/src/general/rpc.ts`.

### Full option coverage

`apply_options(**kwargs)` accepts any option of the underlying API and converts
`snake_case` keys to `camelCase` automatically:

```python
chart.apply_options(auto_size=True, localization={'price_format': {'precision': 4}})
chart.time_scale_options(right_offset=5, bar_spacing=8)   # time scale
series.apply_options(line_width=3, crosshair_marker_visible=False)
get = chart.get_price_scale('left'); get.set_mode('logarithmic')
```

### Indicators

Pure-pandas implementations, no extra dependencies. Overlays go on the price
pane, oscillators get their own pane automatically:

```python
chart.add_sma('close', 20)
chart.add_ema('close', 50, color='#2196F3')
chart.add_bollinger('close', 20, 2)      # -> (upper, middle, lower)
chart.add_donchian(20)
chart.add_vwap()

chart.add_rsi(14)                        # own pane + 70/30 guide lines
chart.add_macd()                         # own pane -> (macd, signal, histogram)
chart.add_stochastic()                   # own pane -> (%K, %D)
chart.add_atr(14)
chart.add_adx(14)                        # own pane -> (adx, +DI, -DI)
chart.add_obv()                          # own pane (needs volume)
chart.add_cci(20)
chart.add_williams_r(14)
chart.add_mfi(14)
chart.add_roc(12)
chart.add_keltner(20, 2.0)               # overlay -> (upper, middle, lower)
```

Raw values: `from pylightcharts import indicators; indicators.rsi(close, 14)`.
RSI, OBV, CCI, %R, ROC, MFI and Bollinger match TA-Lib to floating-point
precision; ATR and ADX match within ~1e-4 and ~0.1 respectively (Wilder
accumulation rounding).

#### Indicators follow the data

Every indicator is recomputed automatically: a full re-send on `chart.set()`,
and a single-point update on `chart.update(bar)` (recomputed from a warm-up
window so recursive indicators stay numerically identical to a full run).
`add_computed_series(compute, name, ...)` adds your own.

#### Batching ticks

Each bridge call is one webview round-trip, so wrap bursts in `chart.batch()`:

```python
with chart.batch():
    for tick in ticks:
        chart.update(tick)      # flushed as a single script
```

#### Conflation (very large data)

Conflation is a plain series option, so the bridge already exposes it:

```python
chart.apply_options(enable_conflation=True, precompute_conflation_on_init=True)
series.apply_options(enable_conflation=True)
```

### Custom series (declarative rendering)

The tricky part of a custom series is `draw()`, which runs synchronously every
frame - python cannot take part in that. So the protocol splits the work:
**python computes the shapes once per data update, one generic JS renderer draws
them every frame.**

```python
from pylightcharts import shapes

series = chart.add_custom_series('range bars', pane_index='new')
series.set(df, shapes=lambda row: shapes.range_bar(row['low'], row['high']))
```

Shape builders: `rect` `band` `line` `circle` `text` `polyline`, plus presets
`range_bar` `box_plot` `error_bar`. Vertical anchors are prices, horizontal
offsets are in bar units (`-0.4..0.4` spans a bar).

```python
shapes.box_plot(low, q1, median, q3, high)
shapes.polyline([(0.0, 1.0), (0.4, 2.0)], fill_color='rgba(0,0,0,0.2)')
shapes.text(price, 'label')
```

Each data item may carry `value`, `low`, `high` (autoscale + last value),
`color`, and `shapes`.

### Price lines, data readback, events

```python
line = series.create_price_line(105.0, color='#00e676', title='entry')
line.apply_options(line_visible=False)
line.remove()

series.price_to_coordinate(105)        # <-> series.coordinate_to_price(y)
series.data_by_index(10)               # individual bar
series.data_points()                   # everything the chart holds
series.pop(2); series.last_value_data(); series.bars_in_logical_range(0, 50)
series.series_type(); series.get_pane_index()
series.move_to_pane(2); series.series_order(); series.set_series_order(3)

chart.scroll_to_real_time(); chart.scroll_to_position(3, animated=False)
chart.reset_time_scale(); chart.set_visible_logical_range(0, 60)
chart.get_visible_range(); chart.get_visible_logical_range()
chart.time_to_coordinate(t); chart.coordinate_to_time(x)
chart.logical_to_coordinate(l); chart.coordinate_to_logical(x)
chart.pane_height(0); chart.set_pane_height(300, 0)
chart.pane_stretch_factor(0); chart.pane_size(); chart.pane_series_count(0)
chart.version(); chart.auto_size_active(); chart.remove()
chart.set_crosshair_position(100.0, some_time); chart.clear_crosshair_position()

chart.get_price_scale('right').get_visible_range()
chart.get_price_scale('right').set_auto_scale(False)
chart.pane_price_scale(0, 'left')

chart.events.crosshair_move += handler      # handler(chart, time, price)
chart.events.dblclick += handler
chart.events.click.unsubscribe()
```

### Multi-chart sync

```python
price = Chart(); volume = Chart()
price.sync(volume)                       # volume follows price pan/zoom
price.sync(volume, crosshairs_only=True)
```

### Native panes (lightweight-charts v5)

```python
chart.add_pane()
chart.create_area(name='close', pane_index=1)
chart.add_series('Line', 'ma20', pane_index=1, color='#0ff')
chart.set_pane_stretch(1, 1.5)
chart.pane_count()
```

`pane_index='new'` creates a fresh pane, and passing an index beyond the
current pane count creates the panes in between.

### Drawing tools

Trend line / horizontal line / vertical line / ray / box, plus:

```python
chart.fibonacci(t1, p1, t2, p2, levels=(0, 0.382, 0.5, 0.618, 1))
chart.measure(t1, p1, t2, p2)                       # price %, bar count
chart.parallel_channel(t1, p1, t2, p2, offset=5)    # second line 5 price units away
chart.position(t1, entry, t2, target, risk_ratio=1.5)   # profit + stop zones
chart.long_position(t1, entry, t2, target)          # aliases
chart.short_position(t1, entry, t2, target)

chart.andrews_pitchfork(t1, p1, t2, p2, t3, p3)     # 3 points
chart.triangle(t1, p1, t2, p2, t3, p3)              # 3 points
chart.fibonacci_extension(t1, p1, t2, p2, t3, p3)   # impulse + retracement
chart.gann_fan(t1, p1, t2, p2)                      # 1x1 plus steeper/shallower rays
```

With `toolbox=True` these are also available interactively
(Alt+F / M / C / P / A / G / X / N).

### Numeric axes and formatters

```python
from pylightcharts import YieldCurveChart, OptionsChart

curve = YieldCurveChart()                 # x axis = duration in months
series = curve.add_series('Line', 'rate')
series.set(pd.DataFrame({'time': [1, 3, 12], 'rate': [4.2, 4.0, 3.6]}))

surface = OptionsChart()                  # x axis = strike

chart.set_price_formatter(decimals=2, thousands=True, prefix='$')
chart.set_time_formatter('YYYY-MM-DD HH:mm')
chart.register_js_formatter('eur', "value => '\u20ac' + value.toFixed(2)")
chart.set_price_formatter(name='eur')
```

### Large data

DataFrames with 2000+ rows are transferred as a base64 column-major Float64
buffer and rebuilt in the browser (`Lib.decodeData`) instead of being shipped as
JavaScript source. Measured 100k bars: **1.56s -> 0.54s (~3x)**;
500k bars: **8.9s -> 3.2s**. Tune with `pylightcharts.util.BINARY_DATA_THRESHOLD`.

### Headless / server-side rendering

```python
from pylightcharts.headless import HeadlessChart

chart = HeadlessChart(width=1200, height=700)
chart.set(df)
chart.add_sma('close', 20)
chart.add_rsi(14)
chart.render('report.png')        # PNG bytes, no window required
chart.to_html()                   # or grab the standalone HTML
```

Uses Playwright when installed, otherwise a local Chrome/Edge in headless mode
(`PYLIGHTCHARTS_CHROME` overrides the browser path).
