pnf-chart-system 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pnf_chart_system-0.1.1/MANIFEST.in +3 -0
- pnf_chart_system-0.1.1/PKG-INFO +201 -0
- pnf_chart_system-0.1.1/README.md +165 -0
- pnf_chart_system-0.1.1/VERSION +1 -0
- pnf_chart_system-0.1.1/pnf_chart_system.egg-info/PKG-INFO +201 -0
- pnf_chart_system-0.1.1/pnf_chart_system.egg-info/SOURCES.txt +9 -0
- pnf_chart_system-0.1.1/pnf_chart_system.egg-info/dependency_links.txt +1 -0
- pnf_chart_system-0.1.1/pnf_chart_system.egg-info/top_level.txt +2 -0
- pnf_chart_system-0.1.1/pypnf_dashboard.py +424 -0
- pnf_chart_system-0.1.1/setup.cfg +4 -0
- pnf_chart_system-0.1.1/setup.py +167 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pnf-chart-system
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Point and Figure Chart Library - Python bindings
|
|
5
|
+
Home-page: https://github.com/gregorian-09/pnf-chart-system
|
|
6
|
+
Author: Gregorian Rayne
|
|
7
|
+
Author-email: gregorianrayne09@gmail.com
|
|
8
|
+
Project-URL: Documentation, https://github.com/gregorian-09/pnf-chart-system/tree/master/docs
|
|
9
|
+
Project-URL: API Reference, https://github.com/gregorian-09/pnf-chart-system/blob/master/docs/bindings/python.md
|
|
10
|
+
Project-URL: Changelog, https://github.com/gregorian-09/pnf-chart-system/blob/master/CHANGELOG.md
|
|
11
|
+
Project-URL: Issues, https://github.com/gregorian-09/pnf-chart-system/issues
|
|
12
|
+
Project-URL: Source, https://github.com/gregorian-09/pnf-chart-system
|
|
13
|
+
Keywords: point-and-figure,charting,technical-analysis,trading,indicators
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: classifier
|
|
29
|
+
Dynamic: description
|
|
30
|
+
Dynamic: description-content-type
|
|
31
|
+
Dynamic: home-page
|
|
32
|
+
Dynamic: keywords
|
|
33
|
+
Dynamic: project-url
|
|
34
|
+
Dynamic: requires-python
|
|
35
|
+
Dynamic: summary
|
|
36
|
+
|
|
37
|
+
# pnf-chart-system
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/pnf-chart-system/)
|
|
40
|
+
[](https://pypi.org/project/pnf-chart-system/)
|
|
41
|
+
[](https://github.com/gregorian-09/pnf-chart-system/blob/master/LICENSE)
|
|
42
|
+
|
|
43
|
+
Production-ready Python bindings for the PnF (Point and Figure) engine.
|
|
44
|
+
|
|
45
|
+
Package name is `pnf-chart-system`; import name is `pypnf`.
|
|
46
|
+
|
|
47
|
+
## Why This Package
|
|
48
|
+
|
|
49
|
+
`pypnf` is built for real analysis workflows, not only chart construction:
|
|
50
|
+
|
|
51
|
+
| Area | What you get |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| Chart Engine | Point-and-Figure charting with `Close` and `HighLow` construction |
|
|
54
|
+
| Trend Context | Bullish support / bearish resistance context checks |
|
|
55
|
+
| Indicators | SMA, Bollinger Bands, RSI, OBV, Bullish Percent |
|
|
56
|
+
| Structural Signals | Buy/sell signals and full PnF pattern detection |
|
|
57
|
+
| Market Structure | Support/resistance levels, price objectives, congestion zones |
|
|
58
|
+
| Visualization | Localhost real-time dashboard streaming |
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install pnf-chart-system
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import pypnf
|
|
70
|
+
|
|
71
|
+
cfg = pypnf.ChartConfig()
|
|
72
|
+
cfg.method = pypnf.ConstructionMethod.HighLow
|
|
73
|
+
cfg.box_size_method = pypnf.BoxSizeMethod.Traditional
|
|
74
|
+
cfg.box_size = 0.0
|
|
75
|
+
cfg.reversal = 3
|
|
76
|
+
|
|
77
|
+
chart = pypnf.Chart(cfg)
|
|
78
|
+
|
|
79
|
+
# high, low, close, timestamp
|
|
80
|
+
chart.add_data(5000.0, 4950.0, 4985.0, 1700000000)
|
|
81
|
+
chart.add_data(5040.0, 4980.0, 5030.0, 1700003600)
|
|
82
|
+
chart.add_data(5065.0, 5010.0, 5055.0, 1700007200)
|
|
83
|
+
|
|
84
|
+
indicators = pypnf.Indicators(pypnf.IndicatorConfig())
|
|
85
|
+
indicators.calculate(chart)
|
|
86
|
+
|
|
87
|
+
print(chart.to_ascii())
|
|
88
|
+
print(indicators.summary())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Trendline and Bias Workflow
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
last_price = 5055.0
|
|
95
|
+
|
|
96
|
+
print("Bullish bias:", chart.has_bullish_bias())
|
|
97
|
+
print("Bearish bias:", chart.has_bearish_bias())
|
|
98
|
+
print("Above bullish support:", chart.is_above_bullish_support(last_price))
|
|
99
|
+
print("Below bearish resistance:", chart.is_below_bearish_resistance(last_price))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
These checks are the normal first gate before acting on breakout or breakdown patterns.
|
|
103
|
+
|
|
104
|
+
## Indicators and Momentum
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
indicators.calculate(chart)
|
|
108
|
+
|
|
109
|
+
sma_short = indicators.sma_short()
|
|
110
|
+
bands = indicators.bollinger()
|
|
111
|
+
rsi = indicators.rsi()
|
|
112
|
+
obv = indicators.obv()
|
|
113
|
+
|
|
114
|
+
col = chart.column_count() - 1
|
|
115
|
+
if col >= 0:
|
|
116
|
+
print("SMA short:", sma_short.value(col))
|
|
117
|
+
print("Bollinger upper:", bands.upper(col))
|
|
118
|
+
print("RSI:", rsi.value(col))
|
|
119
|
+
print("OBV:", obv.value(col))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Signals and Pattern Detection
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
signals = indicators.signals()
|
|
126
|
+
patterns = indicators.patterns()
|
|
127
|
+
|
|
128
|
+
print("Current signal:", signals.current_signal())
|
|
129
|
+
print("Buy count:", signals.buy_count())
|
|
130
|
+
print("Sell count:", signals.sell_count())
|
|
131
|
+
|
|
132
|
+
print("Pattern count:", patterns.pattern_count())
|
|
133
|
+
print("Bullish patterns:", len(patterns.bullish_patterns()))
|
|
134
|
+
print("Bearish patterns:", len(patterns.bearish_patterns()))
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Support, Resistance, Objectives, Congestion
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
sr = indicators.support_resistance()
|
|
141
|
+
obj = indicators.objectives()
|
|
142
|
+
cong = indicators.congestion()
|
|
143
|
+
|
|
144
|
+
print("Support levels:", sr.support_levels())
|
|
145
|
+
print("Resistance levels:", sr.resistance_levels())
|
|
146
|
+
print("Significant levels (>=3 touches):", sr.significant_levels(3))
|
|
147
|
+
|
|
148
|
+
print("Bullish targets:", obj.bullish_targets())
|
|
149
|
+
print("Bearish targets:", obj.bearish_targets())
|
|
150
|
+
|
|
151
|
+
print("Congestion zones:", cong.zones())
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Real-Time Dashboard
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from pypnf_dashboard import DashboardServer
|
|
158
|
+
|
|
159
|
+
server = DashboardServer(chart, indicators)
|
|
160
|
+
server.start("127.0.0.1", 8761)
|
|
161
|
+
server.publish()
|
|
162
|
+
print(server.url())
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
You can call `server.publish()` after each new bar/tick batch to keep the browser in sync.
|
|
166
|
+
|
|
167
|
+
## API Map
|
|
168
|
+
|
|
169
|
+
Core:
|
|
170
|
+
- `Chart`, `ChartConfig`, `Box`, `Column`
|
|
171
|
+
|
|
172
|
+
Indicators:
|
|
173
|
+
- `Indicators`, `IndicatorConfig`
|
|
174
|
+
- `MovingAverage`, `BollingerBands`, `RSI`, `OnBalanceVolume`, `BullishPercent`
|
|
175
|
+
- `SignalDetector`, `PatternRecognizer`, `SupportResistance`, `PriceObjectiveCalculator`, `CongestionDetector`
|
|
176
|
+
|
|
177
|
+
Data:
|
|
178
|
+
- `OHLC`, `Signal`, `Pattern`, `SupportResistanceLevel`, `PriceObjective`, `CongestionZone`
|
|
179
|
+
|
|
180
|
+
Enums:
|
|
181
|
+
- `BoxType`, `ColumnType`, `ConstructionMethod`, `BoxSizeMethod`, `SignalType`, `PatternType`
|
|
182
|
+
|
|
183
|
+
## Versioning and Compatibility
|
|
184
|
+
|
|
185
|
+
- Python package version tracks the same release as the core engine.
|
|
186
|
+
- Keep all bindings on the same version when mixing languages in one system.
|
|
187
|
+
- See `CHANGELOG.md` for version-by-version behavior changes.
|
|
188
|
+
|
|
189
|
+
## Troubleshooting
|
|
190
|
+
|
|
191
|
+
- `ImportError` / native load issues: rebuild and ensure the native library is discoverable.
|
|
192
|
+
- Empty indicator values: verify you have enough columns for the configured lookback periods.
|
|
193
|
+
- Unexpected chart shape: ensure `HighLow` mode receives real high/low values, not close-only values.
|
|
194
|
+
|
|
195
|
+
## Documentation and Links
|
|
196
|
+
|
|
197
|
+
- Python API reference: `docs/bindings/python.md`
|
|
198
|
+
- Cross-language API index: `docs/reference/api-symbol-index.md`
|
|
199
|
+
- Source: https://github.com/gregorian-09/pnf-chart-system
|
|
200
|
+
- Issues: https://github.com/gregorian-09/pnf-chart-system/issues
|
|
201
|
+
- Changelog: https://github.com/gregorian-09/pnf-chart-system/blob/master/CHANGELOG.md
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# pnf-chart-system
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pnf-chart-system/)
|
|
4
|
+
[](https://pypi.org/project/pnf-chart-system/)
|
|
5
|
+
[](https://github.com/gregorian-09/pnf-chart-system/blob/master/LICENSE)
|
|
6
|
+
|
|
7
|
+
Production-ready Python bindings for the PnF (Point and Figure) engine.
|
|
8
|
+
|
|
9
|
+
Package name is `pnf-chart-system`; import name is `pypnf`.
|
|
10
|
+
|
|
11
|
+
## Why This Package
|
|
12
|
+
|
|
13
|
+
`pypnf` is built for real analysis workflows, not only chart construction:
|
|
14
|
+
|
|
15
|
+
| Area | What you get |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| Chart Engine | Point-and-Figure charting with `Close` and `HighLow` construction |
|
|
18
|
+
| Trend Context | Bullish support / bearish resistance context checks |
|
|
19
|
+
| Indicators | SMA, Bollinger Bands, RSI, OBV, Bullish Percent |
|
|
20
|
+
| Structural Signals | Buy/sell signals and full PnF pattern detection |
|
|
21
|
+
| Market Structure | Support/resistance levels, price objectives, congestion zones |
|
|
22
|
+
| Visualization | Localhost real-time dashboard streaming |
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install pnf-chart-system
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import pypnf
|
|
34
|
+
|
|
35
|
+
cfg = pypnf.ChartConfig()
|
|
36
|
+
cfg.method = pypnf.ConstructionMethod.HighLow
|
|
37
|
+
cfg.box_size_method = pypnf.BoxSizeMethod.Traditional
|
|
38
|
+
cfg.box_size = 0.0
|
|
39
|
+
cfg.reversal = 3
|
|
40
|
+
|
|
41
|
+
chart = pypnf.Chart(cfg)
|
|
42
|
+
|
|
43
|
+
# high, low, close, timestamp
|
|
44
|
+
chart.add_data(5000.0, 4950.0, 4985.0, 1700000000)
|
|
45
|
+
chart.add_data(5040.0, 4980.0, 5030.0, 1700003600)
|
|
46
|
+
chart.add_data(5065.0, 5010.0, 5055.0, 1700007200)
|
|
47
|
+
|
|
48
|
+
indicators = pypnf.Indicators(pypnf.IndicatorConfig())
|
|
49
|
+
indicators.calculate(chart)
|
|
50
|
+
|
|
51
|
+
print(chart.to_ascii())
|
|
52
|
+
print(indicators.summary())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Trendline and Bias Workflow
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
last_price = 5055.0
|
|
59
|
+
|
|
60
|
+
print("Bullish bias:", chart.has_bullish_bias())
|
|
61
|
+
print("Bearish bias:", chart.has_bearish_bias())
|
|
62
|
+
print("Above bullish support:", chart.is_above_bullish_support(last_price))
|
|
63
|
+
print("Below bearish resistance:", chart.is_below_bearish_resistance(last_price))
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
These checks are the normal first gate before acting on breakout or breakdown patterns.
|
|
67
|
+
|
|
68
|
+
## Indicators and Momentum
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
indicators.calculate(chart)
|
|
72
|
+
|
|
73
|
+
sma_short = indicators.sma_short()
|
|
74
|
+
bands = indicators.bollinger()
|
|
75
|
+
rsi = indicators.rsi()
|
|
76
|
+
obv = indicators.obv()
|
|
77
|
+
|
|
78
|
+
col = chart.column_count() - 1
|
|
79
|
+
if col >= 0:
|
|
80
|
+
print("SMA short:", sma_short.value(col))
|
|
81
|
+
print("Bollinger upper:", bands.upper(col))
|
|
82
|
+
print("RSI:", rsi.value(col))
|
|
83
|
+
print("OBV:", obv.value(col))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Signals and Pattern Detection
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
signals = indicators.signals()
|
|
90
|
+
patterns = indicators.patterns()
|
|
91
|
+
|
|
92
|
+
print("Current signal:", signals.current_signal())
|
|
93
|
+
print("Buy count:", signals.buy_count())
|
|
94
|
+
print("Sell count:", signals.sell_count())
|
|
95
|
+
|
|
96
|
+
print("Pattern count:", patterns.pattern_count())
|
|
97
|
+
print("Bullish patterns:", len(patterns.bullish_patterns()))
|
|
98
|
+
print("Bearish patterns:", len(patterns.bearish_patterns()))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Support, Resistance, Objectives, Congestion
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
sr = indicators.support_resistance()
|
|
105
|
+
obj = indicators.objectives()
|
|
106
|
+
cong = indicators.congestion()
|
|
107
|
+
|
|
108
|
+
print("Support levels:", sr.support_levels())
|
|
109
|
+
print("Resistance levels:", sr.resistance_levels())
|
|
110
|
+
print("Significant levels (>=3 touches):", sr.significant_levels(3))
|
|
111
|
+
|
|
112
|
+
print("Bullish targets:", obj.bullish_targets())
|
|
113
|
+
print("Bearish targets:", obj.bearish_targets())
|
|
114
|
+
|
|
115
|
+
print("Congestion zones:", cong.zones())
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Real-Time Dashboard
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from pypnf_dashboard import DashboardServer
|
|
122
|
+
|
|
123
|
+
server = DashboardServer(chart, indicators)
|
|
124
|
+
server.start("127.0.0.1", 8761)
|
|
125
|
+
server.publish()
|
|
126
|
+
print(server.url())
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
You can call `server.publish()` after each new bar/tick batch to keep the browser in sync.
|
|
130
|
+
|
|
131
|
+
## API Map
|
|
132
|
+
|
|
133
|
+
Core:
|
|
134
|
+
- `Chart`, `ChartConfig`, `Box`, `Column`
|
|
135
|
+
|
|
136
|
+
Indicators:
|
|
137
|
+
- `Indicators`, `IndicatorConfig`
|
|
138
|
+
- `MovingAverage`, `BollingerBands`, `RSI`, `OnBalanceVolume`, `BullishPercent`
|
|
139
|
+
- `SignalDetector`, `PatternRecognizer`, `SupportResistance`, `PriceObjectiveCalculator`, `CongestionDetector`
|
|
140
|
+
|
|
141
|
+
Data:
|
|
142
|
+
- `OHLC`, `Signal`, `Pattern`, `SupportResistanceLevel`, `PriceObjective`, `CongestionZone`
|
|
143
|
+
|
|
144
|
+
Enums:
|
|
145
|
+
- `BoxType`, `ColumnType`, `ConstructionMethod`, `BoxSizeMethod`, `SignalType`, `PatternType`
|
|
146
|
+
|
|
147
|
+
## Versioning and Compatibility
|
|
148
|
+
|
|
149
|
+
- Python package version tracks the same release as the core engine.
|
|
150
|
+
- Keep all bindings on the same version when mixing languages in one system.
|
|
151
|
+
- See `CHANGELOG.md` for version-by-version behavior changes.
|
|
152
|
+
|
|
153
|
+
## Troubleshooting
|
|
154
|
+
|
|
155
|
+
- `ImportError` / native load issues: rebuild and ensure the native library is discoverable.
|
|
156
|
+
- Empty indicator values: verify you have enough columns for the configured lookback periods.
|
|
157
|
+
- Unexpected chart shape: ensure `HighLow` mode receives real high/low values, not close-only values.
|
|
158
|
+
|
|
159
|
+
## Documentation and Links
|
|
160
|
+
|
|
161
|
+
- Python API reference: `docs/bindings/python.md`
|
|
162
|
+
- Cross-language API index: `docs/reference/api-symbol-index.md`
|
|
163
|
+
- Source: https://github.com/gregorian-09/pnf-chart-system
|
|
164
|
+
- Issues: https://github.com/gregorian-09/pnf-chart-system/issues
|
|
165
|
+
- Changelog: https://github.com/gregorian-09/pnf-chart-system/blob/master/CHANGELOG.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.1
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pnf-chart-system
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Point and Figure Chart Library - Python bindings
|
|
5
|
+
Home-page: https://github.com/gregorian-09/pnf-chart-system
|
|
6
|
+
Author: Gregorian Rayne
|
|
7
|
+
Author-email: gregorianrayne09@gmail.com
|
|
8
|
+
Project-URL: Documentation, https://github.com/gregorian-09/pnf-chart-system/tree/master/docs
|
|
9
|
+
Project-URL: API Reference, https://github.com/gregorian-09/pnf-chart-system/blob/master/docs/bindings/python.md
|
|
10
|
+
Project-URL: Changelog, https://github.com/gregorian-09/pnf-chart-system/blob/master/CHANGELOG.md
|
|
11
|
+
Project-URL: Issues, https://github.com/gregorian-09/pnf-chart-system/issues
|
|
12
|
+
Project-URL: Source, https://github.com/gregorian-09/pnf-chart-system
|
|
13
|
+
Keywords: point-and-figure,charting,technical-analysis,trading,indicators
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: classifier
|
|
29
|
+
Dynamic: description
|
|
30
|
+
Dynamic: description-content-type
|
|
31
|
+
Dynamic: home-page
|
|
32
|
+
Dynamic: keywords
|
|
33
|
+
Dynamic: project-url
|
|
34
|
+
Dynamic: requires-python
|
|
35
|
+
Dynamic: summary
|
|
36
|
+
|
|
37
|
+
# pnf-chart-system
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/pnf-chart-system/)
|
|
40
|
+
[](https://pypi.org/project/pnf-chart-system/)
|
|
41
|
+
[](https://github.com/gregorian-09/pnf-chart-system/blob/master/LICENSE)
|
|
42
|
+
|
|
43
|
+
Production-ready Python bindings for the PnF (Point and Figure) engine.
|
|
44
|
+
|
|
45
|
+
Package name is `pnf-chart-system`; import name is `pypnf`.
|
|
46
|
+
|
|
47
|
+
## Why This Package
|
|
48
|
+
|
|
49
|
+
`pypnf` is built for real analysis workflows, not only chart construction:
|
|
50
|
+
|
|
51
|
+
| Area | What you get |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| Chart Engine | Point-and-Figure charting with `Close` and `HighLow` construction |
|
|
54
|
+
| Trend Context | Bullish support / bearish resistance context checks |
|
|
55
|
+
| Indicators | SMA, Bollinger Bands, RSI, OBV, Bullish Percent |
|
|
56
|
+
| Structural Signals | Buy/sell signals and full PnF pattern detection |
|
|
57
|
+
| Market Structure | Support/resistance levels, price objectives, congestion zones |
|
|
58
|
+
| Visualization | Localhost real-time dashboard streaming |
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install pnf-chart-system
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import pypnf
|
|
70
|
+
|
|
71
|
+
cfg = pypnf.ChartConfig()
|
|
72
|
+
cfg.method = pypnf.ConstructionMethod.HighLow
|
|
73
|
+
cfg.box_size_method = pypnf.BoxSizeMethod.Traditional
|
|
74
|
+
cfg.box_size = 0.0
|
|
75
|
+
cfg.reversal = 3
|
|
76
|
+
|
|
77
|
+
chart = pypnf.Chart(cfg)
|
|
78
|
+
|
|
79
|
+
# high, low, close, timestamp
|
|
80
|
+
chart.add_data(5000.0, 4950.0, 4985.0, 1700000000)
|
|
81
|
+
chart.add_data(5040.0, 4980.0, 5030.0, 1700003600)
|
|
82
|
+
chart.add_data(5065.0, 5010.0, 5055.0, 1700007200)
|
|
83
|
+
|
|
84
|
+
indicators = pypnf.Indicators(pypnf.IndicatorConfig())
|
|
85
|
+
indicators.calculate(chart)
|
|
86
|
+
|
|
87
|
+
print(chart.to_ascii())
|
|
88
|
+
print(indicators.summary())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Trendline and Bias Workflow
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
last_price = 5055.0
|
|
95
|
+
|
|
96
|
+
print("Bullish bias:", chart.has_bullish_bias())
|
|
97
|
+
print("Bearish bias:", chart.has_bearish_bias())
|
|
98
|
+
print("Above bullish support:", chart.is_above_bullish_support(last_price))
|
|
99
|
+
print("Below bearish resistance:", chart.is_below_bearish_resistance(last_price))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
These checks are the normal first gate before acting on breakout or breakdown patterns.
|
|
103
|
+
|
|
104
|
+
## Indicators and Momentum
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
indicators.calculate(chart)
|
|
108
|
+
|
|
109
|
+
sma_short = indicators.sma_short()
|
|
110
|
+
bands = indicators.bollinger()
|
|
111
|
+
rsi = indicators.rsi()
|
|
112
|
+
obv = indicators.obv()
|
|
113
|
+
|
|
114
|
+
col = chart.column_count() - 1
|
|
115
|
+
if col >= 0:
|
|
116
|
+
print("SMA short:", sma_short.value(col))
|
|
117
|
+
print("Bollinger upper:", bands.upper(col))
|
|
118
|
+
print("RSI:", rsi.value(col))
|
|
119
|
+
print("OBV:", obv.value(col))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Signals and Pattern Detection
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
signals = indicators.signals()
|
|
126
|
+
patterns = indicators.patterns()
|
|
127
|
+
|
|
128
|
+
print("Current signal:", signals.current_signal())
|
|
129
|
+
print("Buy count:", signals.buy_count())
|
|
130
|
+
print("Sell count:", signals.sell_count())
|
|
131
|
+
|
|
132
|
+
print("Pattern count:", patterns.pattern_count())
|
|
133
|
+
print("Bullish patterns:", len(patterns.bullish_patterns()))
|
|
134
|
+
print("Bearish patterns:", len(patterns.bearish_patterns()))
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Support, Resistance, Objectives, Congestion
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
sr = indicators.support_resistance()
|
|
141
|
+
obj = indicators.objectives()
|
|
142
|
+
cong = indicators.congestion()
|
|
143
|
+
|
|
144
|
+
print("Support levels:", sr.support_levels())
|
|
145
|
+
print("Resistance levels:", sr.resistance_levels())
|
|
146
|
+
print("Significant levels (>=3 touches):", sr.significant_levels(3))
|
|
147
|
+
|
|
148
|
+
print("Bullish targets:", obj.bullish_targets())
|
|
149
|
+
print("Bearish targets:", obj.bearish_targets())
|
|
150
|
+
|
|
151
|
+
print("Congestion zones:", cong.zones())
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Real-Time Dashboard
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from pypnf_dashboard import DashboardServer
|
|
158
|
+
|
|
159
|
+
server = DashboardServer(chart, indicators)
|
|
160
|
+
server.start("127.0.0.1", 8761)
|
|
161
|
+
server.publish()
|
|
162
|
+
print(server.url())
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
You can call `server.publish()` after each new bar/tick batch to keep the browser in sync.
|
|
166
|
+
|
|
167
|
+
## API Map
|
|
168
|
+
|
|
169
|
+
Core:
|
|
170
|
+
- `Chart`, `ChartConfig`, `Box`, `Column`
|
|
171
|
+
|
|
172
|
+
Indicators:
|
|
173
|
+
- `Indicators`, `IndicatorConfig`
|
|
174
|
+
- `MovingAverage`, `BollingerBands`, `RSI`, `OnBalanceVolume`, `BullishPercent`
|
|
175
|
+
- `SignalDetector`, `PatternRecognizer`, `SupportResistance`, `PriceObjectiveCalculator`, `CongestionDetector`
|
|
176
|
+
|
|
177
|
+
Data:
|
|
178
|
+
- `OHLC`, `Signal`, `Pattern`, `SupportResistanceLevel`, `PriceObjective`, `CongestionZone`
|
|
179
|
+
|
|
180
|
+
Enums:
|
|
181
|
+
- `BoxType`, `ColumnType`, `ConstructionMethod`, `BoxSizeMethod`, `SignalType`, `PatternType`
|
|
182
|
+
|
|
183
|
+
## Versioning and Compatibility
|
|
184
|
+
|
|
185
|
+
- Python package version tracks the same release as the core engine.
|
|
186
|
+
- Keep all bindings on the same version when mixing languages in one system.
|
|
187
|
+
- See `CHANGELOG.md` for version-by-version behavior changes.
|
|
188
|
+
|
|
189
|
+
## Troubleshooting
|
|
190
|
+
|
|
191
|
+
- `ImportError` / native load issues: rebuild and ensure the native library is discoverable.
|
|
192
|
+
- Empty indicator values: verify you have enough columns for the configured lookback periods.
|
|
193
|
+
- Unexpected chart shape: ensure `HighLow` mode receives real high/low values, not close-only values.
|
|
194
|
+
|
|
195
|
+
## Documentation and Links
|
|
196
|
+
|
|
197
|
+
- Python API reference: `docs/bindings/python.md`
|
|
198
|
+
- Cross-language API index: `docs/reference/api-symbol-index.md`
|
|
199
|
+
- Source: https://github.com/gregorian-09/pnf-chart-system
|
|
200
|
+
- Issues: https://github.com/gregorian-09/pnf-chart-system/issues
|
|
201
|
+
- Changelog: https://github.com/gregorian-09/pnf-chart-system/blob/master/CHANGELOG.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""Localhost real-time dashboard for pypnf."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import pypnf
|
|
18
|
+
|
|
19
|
+
_WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _repo_root() -> Path:
|
|
23
|
+
return Path(__file__).resolve().parents[2]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _asset_path(name: str) -> Path:
|
|
27
|
+
return _repo_root() / "dashboard" / "web" / name
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _enum_name(value: Any) -> str:
|
|
31
|
+
if hasattr(value, "name"):
|
|
32
|
+
return str(value.name)
|
|
33
|
+
return str(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_snapshot(chart: Any, indicators: Any | None = None, sequence: int = 1) -> dict[str, Any]:
|
|
37
|
+
column_count = int(chart.column_count())
|
|
38
|
+
columns = []
|
|
39
|
+
for idx in range(column_count):
|
|
40
|
+
box_count = int(chart.column_box_count(idx))
|
|
41
|
+
boxes = []
|
|
42
|
+
for box_idx in range(box_count):
|
|
43
|
+
boxes.append(
|
|
44
|
+
{
|
|
45
|
+
"index": box_idx,
|
|
46
|
+
"price": float(chart.box_price(idx, box_idx)),
|
|
47
|
+
"type": _enum_name(chart.box_type(idx, box_idx)),
|
|
48
|
+
"marker": str(chart.box_marker(idx, box_idx) or ""),
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
columns.append(
|
|
52
|
+
{
|
|
53
|
+
"index": idx,
|
|
54
|
+
"type": _enum_name(chart.column_type(idx)),
|
|
55
|
+
"box_count": box_count,
|
|
56
|
+
"highest": float(chart.column_high(idx)),
|
|
57
|
+
"lowest": float(chart.column_low(idx)),
|
|
58
|
+
"boxes": boxes,
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
indicator_payload = {
|
|
63
|
+
"summary": "",
|
|
64
|
+
"detailed_summary": "",
|
|
65
|
+
"bullish_percent": 0.0,
|
|
66
|
+
"signal_count": 0,
|
|
67
|
+
"buy_signal_count": 0,
|
|
68
|
+
"sell_signal_count": 0,
|
|
69
|
+
"pattern_count": 0,
|
|
70
|
+
"support_level_count": 0,
|
|
71
|
+
"resistance_level_count": 0,
|
|
72
|
+
"congestion_zone_count": 0,
|
|
73
|
+
"signals": [],
|
|
74
|
+
"patterns": [],
|
|
75
|
+
"support_levels": [],
|
|
76
|
+
"resistance_levels": [],
|
|
77
|
+
"price_objectives": [],
|
|
78
|
+
"congestion_zones": [],
|
|
79
|
+
"series": {
|
|
80
|
+
"sma_short": [],
|
|
81
|
+
"sma_medium": [],
|
|
82
|
+
"sma_long": [],
|
|
83
|
+
"bollinger_middle": [],
|
|
84
|
+
"bollinger_upper": [],
|
|
85
|
+
"bollinger_lower": [],
|
|
86
|
+
"rsi": [],
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
if indicators is not None:
|
|
90
|
+
try:
|
|
91
|
+
signal_detector = indicators.signals()
|
|
92
|
+
pattern_recognizer = indicators.patterns()
|
|
93
|
+
support_resistance = indicators.support_resistance()
|
|
94
|
+
objectives = indicators.objectives()
|
|
95
|
+
congestion = indicators.congestion()
|
|
96
|
+
sma_short = [float(v) for v in indicators.sma_short().values_copy()]
|
|
97
|
+
sma_medium = [float(v) for v in indicators.sma_medium().values_copy()]
|
|
98
|
+
sma_long = [float(v) for v in indicators.sma_long().values_copy()]
|
|
99
|
+
bollinger = indicators.bollinger()
|
|
100
|
+
rsi = indicators.rsi()
|
|
101
|
+
signals = [
|
|
102
|
+
{
|
|
103
|
+
"type": _enum_name(signal.type),
|
|
104
|
+
"column_index": int(signal.column_index),
|
|
105
|
+
"price": float(signal.price),
|
|
106
|
+
}
|
|
107
|
+
for signal in signal_detector.signals()
|
|
108
|
+
]
|
|
109
|
+
patterns = [
|
|
110
|
+
{
|
|
111
|
+
"type": _enum_name(pattern.type),
|
|
112
|
+
"start_column": int(pattern.start_column),
|
|
113
|
+
"end_column": int(pattern.end_column),
|
|
114
|
+
"price": float(pattern.price),
|
|
115
|
+
"is_bullish": bool(pattern.is_bullish()),
|
|
116
|
+
}
|
|
117
|
+
for pattern in pattern_recognizer.patterns()
|
|
118
|
+
]
|
|
119
|
+
support_levels = [
|
|
120
|
+
{
|
|
121
|
+
"type": "Support",
|
|
122
|
+
"price": float(level.price),
|
|
123
|
+
"touches": int(level.touch_count),
|
|
124
|
+
}
|
|
125
|
+
for level in support_resistance.support_levels()
|
|
126
|
+
]
|
|
127
|
+
resistance_levels = [
|
|
128
|
+
{
|
|
129
|
+
"type": "Resistance",
|
|
130
|
+
"price": float(level.price),
|
|
131
|
+
"touches": int(level.touch_count),
|
|
132
|
+
}
|
|
133
|
+
for level in support_resistance.resistance_levels()
|
|
134
|
+
]
|
|
135
|
+
price_objectives = [
|
|
136
|
+
{
|
|
137
|
+
"target": float(objective.target_price),
|
|
138
|
+
"column_index": int(objective.base_column),
|
|
139
|
+
"box_count": int(objective.box_count),
|
|
140
|
+
"is_bullish": bool(objective.is_bullish),
|
|
141
|
+
}
|
|
142
|
+
for objective in objectives.objectives()
|
|
143
|
+
]
|
|
144
|
+
congestion_zones = [
|
|
145
|
+
{
|
|
146
|
+
"start_column": int(zone.start_column),
|
|
147
|
+
"end_column": int(zone.end_column),
|
|
148
|
+
"high_price": float(zone.high_price),
|
|
149
|
+
"low_price": float(zone.low_price),
|
|
150
|
+
"column_count": int(zone.column_count),
|
|
151
|
+
}
|
|
152
|
+
for zone in congestion.zones()
|
|
153
|
+
]
|
|
154
|
+
indicator_payload.update(
|
|
155
|
+
{
|
|
156
|
+
"summary": str(indicators.summary()),
|
|
157
|
+
"detailed_summary": str(indicators),
|
|
158
|
+
"bullish_percent": float(indicators.bullish_percent().value()),
|
|
159
|
+
"signal_count": int(signal_detector.buy_count() + signal_detector.sell_count()),
|
|
160
|
+
"buy_signal_count": int(signal_detector.buy_count()),
|
|
161
|
+
"sell_signal_count": int(signal_detector.sell_count()),
|
|
162
|
+
"pattern_count": int(pattern_recognizer.pattern_count()),
|
|
163
|
+
"support_level_count": len(support_levels),
|
|
164
|
+
"resistance_level_count": len(resistance_levels),
|
|
165
|
+
"congestion_zone_count": len(congestion_zones),
|
|
166
|
+
"signals": signals,
|
|
167
|
+
"patterns": patterns,
|
|
168
|
+
"support_levels": support_levels,
|
|
169
|
+
"resistance_levels": resistance_levels,
|
|
170
|
+
"price_objectives": price_objectives,
|
|
171
|
+
"congestion_zones": congestion_zones,
|
|
172
|
+
"series": {
|
|
173
|
+
"sma_short": sma_short,
|
|
174
|
+
"sma_medium": sma_medium,
|
|
175
|
+
"sma_long": sma_long,
|
|
176
|
+
"bollinger_middle": [float(v) for v in bollinger.middle_copy()],
|
|
177
|
+
"bollinger_upper": [float(v) for v in bollinger.upper_copy()],
|
|
178
|
+
"bollinger_lower": [float(v) for v in bollinger.lower_copy()],
|
|
179
|
+
"rsi": [float(v) for v in rsi.values_copy()],
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
except Exception:
|
|
184
|
+
indicator_payload["summary"] = "Indicator snapshot unavailable"
|
|
185
|
+
indicator_payload["detailed_summary"] = "Detailed indicator snapshot unavailable"
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
"type": "dashboard.snapshot",
|
|
189
|
+
"version": 1,
|
|
190
|
+
"timestamp": int(time.time() * 1000),
|
|
191
|
+
"sequence": sequence,
|
|
192
|
+
"payload": {
|
|
193
|
+
"meta": {
|
|
194
|
+
"binding": "python",
|
|
195
|
+
"library_version": pypnf.version(),
|
|
196
|
+
"server_version": "1",
|
|
197
|
+
"stream_state": "running",
|
|
198
|
+
},
|
|
199
|
+
"chart": {
|
|
200
|
+
"box_size": float(chart.current_box_size()),
|
|
201
|
+
"column_count": column_count,
|
|
202
|
+
"x_column_count": int(chart.x_column_count()),
|
|
203
|
+
"o_column_count": int(chart.o_column_count()),
|
|
204
|
+
"has_bullish_bias": bool(chart.has_bullish_bias()),
|
|
205
|
+
"has_bearish_bias": bool(chart.has_bearish_bias()),
|
|
206
|
+
"columns": columns,
|
|
207
|
+
},
|
|
208
|
+
"indicators": indicator_payload,
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class _DashboardHandler(BaseHTTPRequestHandler):
|
|
214
|
+
server_version = "PnFPythonDashboard/1.0"
|
|
215
|
+
|
|
216
|
+
def do_GET(self) -> None:
|
|
217
|
+
dashboard = self.server.dashboard_server # type: ignore[attr-defined]
|
|
218
|
+
if self.headers.get("Upgrade", "").lower() == "websocket" and self.path == "/ws":
|
|
219
|
+
self._handle_ws(dashboard)
|
|
220
|
+
return
|
|
221
|
+
if self.path == "/healthz":
|
|
222
|
+
self._send_bytes(200, b"ok", "text/plain; charset=utf-8")
|
|
223
|
+
return
|
|
224
|
+
if self.path == "/snapshot":
|
|
225
|
+
self._send_bytes(200, dashboard.snapshot_json().encode("utf-8"), "application/json; charset=utf-8")
|
|
226
|
+
return
|
|
227
|
+
if self.path == "/app.js":
|
|
228
|
+
self._send_file(_asset_path("app.js"), "application/javascript; charset=utf-8")
|
|
229
|
+
return
|
|
230
|
+
if self.path == "/styles.css":
|
|
231
|
+
self._send_file(_asset_path("styles.css"), "text/css; charset=utf-8")
|
|
232
|
+
return
|
|
233
|
+
if self.path == "/" or self.path == "/index.html":
|
|
234
|
+
html = _asset_path("index.html").read_text(encoding="utf-8")
|
|
235
|
+
ws_url = f"ws://{dashboard.host}:{dashboard.port}/ws"
|
|
236
|
+
html = html.replace(
|
|
237
|
+
"window.PNF_DASHBOARD_CONFIG = window.PNF_DASHBOARD_CONFIG || {};",
|
|
238
|
+
f"window.PNF_DASHBOARD_CONFIG = {{ wsUrl: {json.dumps(ws_url)} }};",
|
|
239
|
+
)
|
|
240
|
+
self._send_bytes(200, html.encode("utf-8"), "text/html; charset=utf-8")
|
|
241
|
+
return
|
|
242
|
+
self._send_bytes(404, b"not found", "text/plain; charset=utf-8")
|
|
243
|
+
|
|
244
|
+
def log_message(self, format: str, *args: Any) -> None: # noqa: A003
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
def _send_file(self, path: Path, content_type: str) -> None:
|
|
248
|
+
self._send_bytes(200, path.read_bytes(), content_type)
|
|
249
|
+
|
|
250
|
+
def _send_bytes(self, status: int, payload: bytes, content_type: str) -> None:
|
|
251
|
+
self.send_response(status)
|
|
252
|
+
self.send_header("Content-Type", content_type)
|
|
253
|
+
self.send_header("Content-Length", str(len(payload)))
|
|
254
|
+
self.send_header("Cache-Control", "no-store")
|
|
255
|
+
self.end_headers()
|
|
256
|
+
self.wfile.write(payload)
|
|
257
|
+
|
|
258
|
+
def _handle_ws(self, dashboard: "DashboardServer") -> None:
|
|
259
|
+
key = self.headers.get("Sec-WebSocket-Key")
|
|
260
|
+
if not key:
|
|
261
|
+
self.send_error(400, "Missing Sec-WebSocket-Key")
|
|
262
|
+
return
|
|
263
|
+
accept = base64.b64encode(hashlib.sha1((key + _WS_GUID).encode("utf-8")).digest()).decode("ascii")
|
|
264
|
+
self.connection.sendall(
|
|
265
|
+
(
|
|
266
|
+
"HTTP/1.1 101 Switching Protocols\r\n"
|
|
267
|
+
"Upgrade: websocket\r\n"
|
|
268
|
+
"Connection: Upgrade\r\n"
|
|
269
|
+
f"Sec-WebSocket-Accept: {accept}\r\n\r\n"
|
|
270
|
+
).encode("utf-8")
|
|
271
|
+
)
|
|
272
|
+
self.connection.setblocking(True)
|
|
273
|
+
dashboard._register_client(self.connection)
|
|
274
|
+
try:
|
|
275
|
+
while dashboard.is_running:
|
|
276
|
+
time.sleep(1.0)
|
|
277
|
+
finally:
|
|
278
|
+
dashboard._unregister_client(self.connection)
|
|
279
|
+
try:
|
|
280
|
+
self.connection.close()
|
|
281
|
+
except OSError:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass
|
|
286
|
+
class DashboardServer:
|
|
287
|
+
chart: Any | None = None
|
|
288
|
+
indicators: Any | None = None
|
|
289
|
+
|
|
290
|
+
def __post_init__(self) -> None:
|
|
291
|
+
self.host = "127.0.0.1"
|
|
292
|
+
self.port = 0
|
|
293
|
+
self._server: ThreadingHTTPServer | None = None
|
|
294
|
+
self._thread: threading.Thread | None = None
|
|
295
|
+
self._clients: list[socket.socket] = []
|
|
296
|
+
self._clients_lock = threading.Lock()
|
|
297
|
+
self._sequence = 0
|
|
298
|
+
self._latest = json.dumps({
|
|
299
|
+
"type": "dashboard.snapshot",
|
|
300
|
+
"version": 1,
|
|
301
|
+
"timestamp": int(time.time() * 1000),
|
|
302
|
+
"sequence": 0,
|
|
303
|
+
"payload": {
|
|
304
|
+
"meta": {"binding": "python", "library_version": pypnf.version(), "server_version": "1", "stream_state": "idle"},
|
|
305
|
+
"chart": {"box_size": 0, "column_count": 0, "x_column_count": 0, "o_column_count": 0, "has_bullish_bias": False, "has_bearish_bias": False, "columns": []},
|
|
306
|
+
"indicators": {"summary": "", "detailed_summary": "", "bullish_percent": 0, "signal_count": 0, "buy_signal_count": 0, "sell_signal_count": 0, "pattern_count": 0, "support_level_count": 0, "resistance_level_count": 0, "congestion_zone_count": 0, "signals": [], "patterns": [], "support_levels": [], "resistance_levels": [], "price_objectives": [], "congestion_zones": [], "series": {"sma_short": [], "sma_medium": [], "sma_long": [], "bollinger_middle": [], "bollinger_upper": [], "bollinger_lower": [], "rsi": []}},
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
self._running = threading.Event()
|
|
310
|
+
self._publisher_stop = threading.Event()
|
|
311
|
+
self._publisher_thread: threading.Thread | None = None
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def is_running(self) -> bool:
|
|
315
|
+
return self._running.is_set()
|
|
316
|
+
|
|
317
|
+
def set_chart(self, chart: Any) -> None:
|
|
318
|
+
self.chart = chart
|
|
319
|
+
|
|
320
|
+
def set_indicators(self, indicators: Any) -> None:
|
|
321
|
+
self.indicators = indicators
|
|
322
|
+
|
|
323
|
+
def start(self, host: str = "127.0.0.1", port: int = 0) -> str:
|
|
324
|
+
if self._server is not None:
|
|
325
|
+
return self.url()
|
|
326
|
+
self.host = host
|
|
327
|
+
self._server = ThreadingHTTPServer((host, port), _DashboardHandler)
|
|
328
|
+
self._server.dashboard_server = self # type: ignore[attr-defined]
|
|
329
|
+
self.port = int(self._server.server_address[1])
|
|
330
|
+
self._running.set()
|
|
331
|
+
self._thread = threading.Thread(target=self._server.serve_forever, name="pypnf-dashboard", daemon=True)
|
|
332
|
+
self._thread.start()
|
|
333
|
+
return self.url()
|
|
334
|
+
|
|
335
|
+
def url(self) -> str:
|
|
336
|
+
return f"http://{self.host}:{self.port}/"
|
|
337
|
+
|
|
338
|
+
def snapshot(self) -> dict[str, Any]:
|
|
339
|
+
if self.chart is None:
|
|
340
|
+
return json.loads(self._latest)
|
|
341
|
+
self._sequence += 1
|
|
342
|
+
return build_snapshot(self.chart, self.indicators, self._sequence)
|
|
343
|
+
|
|
344
|
+
def snapshot_json(self) -> str:
|
|
345
|
+
return self._latest
|
|
346
|
+
|
|
347
|
+
def publish(self) -> str:
|
|
348
|
+
snap = self.snapshot()
|
|
349
|
+
self._latest = json.dumps(snap)
|
|
350
|
+
self._broadcast(self._latest)
|
|
351
|
+
return self._latest
|
|
352
|
+
|
|
353
|
+
def start_auto_publish(self, interval_ms: int = 250) -> None:
|
|
354
|
+
self.stop_auto_publish()
|
|
355
|
+
self._publisher_stop.clear()
|
|
356
|
+
|
|
357
|
+
def _loop() -> None:
|
|
358
|
+
while not self._publisher_stop.wait(interval_ms / 1000.0):
|
|
359
|
+
if self.is_running and self.chart is not None:
|
|
360
|
+
self.publish()
|
|
361
|
+
|
|
362
|
+
self._publisher_thread = threading.Thread(target=_loop, name="pypnf-dashboard-publisher", daemon=True)
|
|
363
|
+
self._publisher_thread.start()
|
|
364
|
+
|
|
365
|
+
def stop_auto_publish(self) -> None:
|
|
366
|
+
self._publisher_stop.set()
|
|
367
|
+
self._publisher_thread = None
|
|
368
|
+
|
|
369
|
+
def stop(self) -> None:
|
|
370
|
+
self.stop_auto_publish()
|
|
371
|
+
self._running.clear()
|
|
372
|
+
if self._server is not None:
|
|
373
|
+
self._server.shutdown()
|
|
374
|
+
self._server.server_close()
|
|
375
|
+
self._server = None
|
|
376
|
+
with self._clients_lock:
|
|
377
|
+
clients = list(self._clients)
|
|
378
|
+
self._clients.clear()
|
|
379
|
+
for sock in clients:
|
|
380
|
+
try:
|
|
381
|
+
sock.close()
|
|
382
|
+
except OSError:
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
def _register_client(self, sock: socket.socket) -> None:
|
|
386
|
+
with self._clients_lock:
|
|
387
|
+
self._clients.append(sock)
|
|
388
|
+
if self._latest:
|
|
389
|
+
self._send_ws_text(sock, self._latest)
|
|
390
|
+
|
|
391
|
+
def _unregister_client(self, sock: socket.socket) -> None:
|
|
392
|
+
with self._clients_lock:
|
|
393
|
+
self._clients = [client for client in self._clients if client is not sock]
|
|
394
|
+
|
|
395
|
+
def _broadcast(self, message: str) -> None:
|
|
396
|
+
with self._clients_lock:
|
|
397
|
+
clients = list(self._clients)
|
|
398
|
+
stale = []
|
|
399
|
+
for sock in clients:
|
|
400
|
+
try:
|
|
401
|
+
self._send_ws_text(sock, message)
|
|
402
|
+
except OSError:
|
|
403
|
+
stale.append(sock)
|
|
404
|
+
for sock in stale:
|
|
405
|
+
self._unregister_client(sock)
|
|
406
|
+
try:
|
|
407
|
+
sock.close()
|
|
408
|
+
except OSError:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
@staticmethod
|
|
412
|
+
def _send_ws_text(sock: socket.socket, message: str) -> None:
|
|
413
|
+
payload = message.encode("utf-8")
|
|
414
|
+
length = len(payload)
|
|
415
|
+
if length < 126:
|
|
416
|
+
header = bytes([0x81, length])
|
|
417
|
+
elif length < 65536:
|
|
418
|
+
header = bytes([0x81, 126]) + length.to_bytes(2, "big")
|
|
419
|
+
else:
|
|
420
|
+
header = bytes([0x81, 127]) + length.to_bytes(8, "big")
|
|
421
|
+
sock.sendall(header + payload)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
__all__ = ["DashboardServer", "build_snapshot"]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Setup script for pypnf - Point and Figure Chart Library Python bindings.
|
|
4
|
+
|
|
5
|
+
Build modes:
|
|
6
|
+
1. Standalone (bundles C++ sources): pip install .
|
|
7
|
+
2. System library (links to installed libpnf): pip install . --config-settings=--build-option=--use-system-lib
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from setuptools import setup, Extension
|
|
16
|
+
from setuptools.command.build_ext import build_ext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_project_version() -> str:
|
|
20
|
+
"""Read the canonical project version from the repo root VERSION file."""
|
|
21
|
+
env_version = os.environ.get("PNF_VERSION", "").strip()
|
|
22
|
+
if env_version:
|
|
23
|
+
return env_version
|
|
24
|
+
|
|
25
|
+
candidates = [
|
|
26
|
+
Path(__file__).resolve().parent / "VERSION",
|
|
27
|
+
Path(__file__).resolve().parents[2] / "VERSION",
|
|
28
|
+
]
|
|
29
|
+
for version_file in candidates:
|
|
30
|
+
if version_file.exists():
|
|
31
|
+
return version_file.read_text(encoding="utf-8").strip()
|
|
32
|
+
|
|
33
|
+
raise FileNotFoundError(
|
|
34
|
+
"Could not locate VERSION file. Set PNF_VERSION or provide bindings/python/VERSION."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_readme() -> str:
|
|
39
|
+
"""Read the package README shown on PyPI."""
|
|
40
|
+
return (Path(__file__).resolve().parent / "README.md").read_text(encoding="utf-8")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CMakeExtension(Extension):
|
|
44
|
+
"""Extension that uses CMake for building."""
|
|
45
|
+
def __init__(self, name, sourcedir=""):
|
|
46
|
+
super().__init__(name, sources=[])
|
|
47
|
+
self.sourcedir = os.path.abspath(sourcedir)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CMakeBuild(build_ext):
|
|
51
|
+
"""Build extension using CMake."""
|
|
52
|
+
|
|
53
|
+
def build_extension(self, ext):
|
|
54
|
+
if not isinstance(ext, CMakeExtension):
|
|
55
|
+
super().build_extension(ext)
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
|
|
59
|
+
|
|
60
|
+
# Ensure output directory exists
|
|
61
|
+
if not os.path.exists(extdir):
|
|
62
|
+
os.makedirs(extdir)
|
|
63
|
+
|
|
64
|
+
# CMake configuration
|
|
65
|
+
cmake_args = [
|
|
66
|
+
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}",
|
|
67
|
+
f"-DPYTHON_EXECUTABLE={sys.executable}",
|
|
68
|
+
"-DPNF_BUILD_PYTHON=ON",
|
|
69
|
+
"-DPNF_BUILD_TESTS=OFF",
|
|
70
|
+
"-DPNF_BUILD_EXAMPLES=OFF",
|
|
71
|
+
"-DPNF_BUILD_VIEWER=OFF",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Build type
|
|
75
|
+
cfg = "Debug" if self.debug else "Release"
|
|
76
|
+
cmake_args.append(f"-DCMAKE_BUILD_TYPE={cfg}")
|
|
77
|
+
|
|
78
|
+
build_args = ["--config", cfg]
|
|
79
|
+
|
|
80
|
+
# Parallel build
|
|
81
|
+
if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ:
|
|
82
|
+
build_args += ["--", "-j4"]
|
|
83
|
+
|
|
84
|
+
# Build directory
|
|
85
|
+
build_temp = os.path.join(self.build_temp, ext.name)
|
|
86
|
+
if not os.path.exists(build_temp):
|
|
87
|
+
os.makedirs(build_temp)
|
|
88
|
+
|
|
89
|
+
# Run CMake
|
|
90
|
+
subprocess.check_call(["cmake", ext.sourcedir] + cmake_args, cwd=build_temp)
|
|
91
|
+
subprocess.check_call(["cmake", "--build", ".", "--target", "pypnf"] + build_args, cwd=build_temp)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Alternative: pybind11 direct build (simpler but requires pre-built libpnf)
|
|
95
|
+
def get_pybind11_extension():
|
|
96
|
+
"""Create extension using pybind11 directly (requires system libpnf)."""
|
|
97
|
+
try:
|
|
98
|
+
import pybind11
|
|
99
|
+
pybind11_include = pybind11.get_include()
|
|
100
|
+
except ImportError:
|
|
101
|
+
pybind11_include = ""
|
|
102
|
+
|
|
103
|
+
# Find library paths
|
|
104
|
+
root_directory = Path(__file__).parent.parent.parent
|
|
105
|
+
include_dir = root_directory / "include"
|
|
106
|
+
|
|
107
|
+
return Extension(
|
|
108
|
+
"pypnf",
|
|
109
|
+
sources=["pnf_python.cpp"],
|
|
110
|
+
include_dirs=[
|
|
111
|
+
str(include_dir),
|
|
112
|
+
pybind11_include,
|
|
113
|
+
],
|
|
114
|
+
libraries=["pnf"],
|
|
115
|
+
language="c++",
|
|
116
|
+
extra_compile_args=["-std=c++20", "-O3", "-Wall"],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Determine build mode
|
|
121
|
+
USE_CMAKE = True # Set to False if you want direct pybind11 build with system library
|
|
122
|
+
|
|
123
|
+
if USE_CMAKE:
|
|
124
|
+
# CMake build bundles everything
|
|
125
|
+
root_dir = str(Path(__file__).parent.parent.parent)
|
|
126
|
+
ext_modules = [CMakeExtension("pypnf", sourcedir=root_dir)]
|
|
127
|
+
cmdclass = {"build_ext": CMakeBuild}
|
|
128
|
+
else:
|
|
129
|
+
# Direct pybind11 build requires pre-installed libpnf
|
|
130
|
+
ext_modules = [get_pybind11_extension()]
|
|
131
|
+
cmdclass = {}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
setup(
|
|
135
|
+
name="pnf-chart-system",
|
|
136
|
+
version=read_project_version(),
|
|
137
|
+
author="Gregorian Rayne",
|
|
138
|
+
author_email="gregorianrayne09@gmail.com",
|
|
139
|
+
description="Point and Figure Chart Library - Python bindings",
|
|
140
|
+
long_description=read_readme(),
|
|
141
|
+
long_description_content_type="text/markdown",
|
|
142
|
+
url="https://github.com/gregorian-09/pnf-chart-system",
|
|
143
|
+
project_urls={
|
|
144
|
+
"Documentation": "https://github.com/gregorian-09/pnf-chart-system/tree/master/docs",
|
|
145
|
+
"API Reference": "https://github.com/gregorian-09/pnf-chart-system/blob/master/docs/bindings/python.md",
|
|
146
|
+
"Changelog": "https://github.com/gregorian-09/pnf-chart-system/blob/master/CHANGELOG.md",
|
|
147
|
+
"Issues": "https://github.com/gregorian-09/pnf-chart-system/issues",
|
|
148
|
+
"Source": "https://github.com/gregorian-09/pnf-chart-system",
|
|
149
|
+
},
|
|
150
|
+
ext_modules=ext_modules,
|
|
151
|
+
py_modules=["pypnf_dashboard"],
|
|
152
|
+
cmdclass=cmdclass,
|
|
153
|
+
python_requires=">=3.8",
|
|
154
|
+
keywords="point-and-figure, charting, technical-analysis, trading, indicators",
|
|
155
|
+
classifiers=[
|
|
156
|
+
"Development Status :: 4 - Beta",
|
|
157
|
+
"Intended Audience :: Developers",
|
|
158
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
159
|
+
"License :: OSI Approved :: MIT License",
|
|
160
|
+
"Programming Language :: Python :: 3",
|
|
161
|
+
"Programming Language :: Python :: 3.8",
|
|
162
|
+
"Programming Language :: Python :: 3.9",
|
|
163
|
+
"Programming Language :: Python :: 3.10",
|
|
164
|
+
"Programming Language :: Python :: 3.11",
|
|
165
|
+
"Programming Language :: Python :: 3.12",
|
|
166
|
+
],
|
|
167
|
+
)
|