Skip to content

Commit e4fafdf

Browse files
feat(bokeh): implement point-and-figure-basic (#3861)
## Implementation: `point-and-figure-basic` - bokeh Implements the **bokeh** version of `point-and-figure-basic`. **File:** `plots/point-and-figure-basic/implementations/bokeh.py` **Parent Issue:** #3755 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/21047207311)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 66819c4 commit e4fafdf

2 files changed

Lines changed: 428 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
""" pyplots.ai
2+
point-and-figure-basic: Point and Figure Chart
3+
Library: bokeh 3.8.2 | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-15
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
from bokeh.io import export_png, save
10+
from bokeh.models import ColumnDataSource
11+
from bokeh.plotting import figure
12+
13+
14+
# Generate synthetic stock price data
15+
np.random.seed(42)
16+
n_days = 300
17+
18+
# Start price and generate realistic daily returns
19+
start_price = 100
20+
daily_returns = np.random.normal(0.0005, 0.015, n_days)
21+
22+
# Add some trending periods
23+
daily_returns[50:80] += 0.003 # Uptrend
24+
daily_returns[100:140] -= 0.004 # Downtrend
25+
daily_returns[180:220] += 0.0035 # Uptrend
26+
daily_returns[240:280] -= 0.003 # Downtrend
27+
28+
close_prices = start_price * np.cumprod(1 + daily_returns)
29+
30+
# Generate high/low based on close
31+
volatility = np.abs(np.random.normal(0, 0.01, n_days))
32+
high_prices = close_prices * (1 + volatility)
33+
low_prices = close_prices * (1 - volatility)
34+
35+
dates = pd.date_range("2024-01-01", periods=n_days, freq="D")
36+
37+
df = pd.DataFrame({"date": dates, "high": high_prices, "low": low_prices, "close": close_prices})
38+
39+
# Point and Figure calculation
40+
box_size = 2.0 # Each box represents $2
41+
reversal = 3 # 3-box reversal
42+
43+
# Calculate P&F columns
44+
columns = []
45+
current_direction = None # 'X' for up, 'O' for down
46+
current_column_start = None
47+
current_column_end = None
48+
49+
# Initialize with first price
50+
first_price = df["close"].iloc[0]
51+
box_start = np.floor(first_price / box_size) * box_size
52+
53+
for _i, row in df.iterrows():
54+
price = row["close"]
55+
box_price = np.floor(price / box_size) * box_size
56+
57+
if current_direction is None:
58+
# Initialize first direction based on next movement
59+
current_column_start = box_start
60+
current_column_end = box_start
61+
# Wait for significant move
62+
if price >= box_start + box_size:
63+
current_direction = "X"
64+
current_column_end = box_price
65+
elif price <= box_start - box_size:
66+
current_direction = "O"
67+
current_column_end = box_price
68+
else:
69+
if current_direction == "X":
70+
# In uptrend
71+
if box_price >= current_column_end + box_size:
72+
# Continue up
73+
current_column_end = box_price
74+
elif box_price <= current_column_end - reversal * box_size:
75+
# Reversal down - save current column and start new O column
76+
columns.append({"type": "X", "start": current_column_start, "end": current_column_end})
77+
current_direction = "O"
78+
current_column_start = current_column_end - box_size
79+
current_column_end = box_price
80+
else:
81+
# In downtrend
82+
if box_price <= current_column_end - box_size:
83+
# Continue down
84+
current_column_end = box_price
85+
elif box_price >= current_column_end + reversal * box_size:
86+
# Reversal up - save current column and start new X column
87+
columns.append({"type": "O", "start": current_column_start, "end": current_column_end})
88+
current_direction = "X"
89+
current_column_start = current_column_end + box_size
90+
current_column_end = box_price
91+
92+
# Save the last column
93+
if current_direction is not None:
94+
columns.append({"type": current_direction, "start": current_column_start, "end": current_column_end})
95+
96+
# Prepare data for plotting
97+
x_data = []
98+
o_data = []
99+
100+
for col_idx, col in enumerate(columns):
101+
if col["type"] == "X":
102+
start = min(col["start"], col["end"])
103+
end = max(col["start"], col["end"])
104+
boxes = np.arange(start, end + box_size / 2, box_size)
105+
for box in boxes:
106+
x_data.append({"col": col_idx, "price": box})
107+
else:
108+
start = max(col["start"], col["end"])
109+
end = min(col["start"], col["end"])
110+
boxes = np.arange(end, start + box_size / 2, box_size)
111+
for box in boxes:
112+
o_data.append({"col": col_idx, "price": box})
113+
114+
# Create figure
115+
p = figure(
116+
width=4800,
117+
height=2700,
118+
title="point-and-figure-basic · bokeh · pyplots.ai",
119+
x_axis_label="Column (Reversal)",
120+
y_axis_label="Price ($)",
121+
)
122+
123+
# Style settings - scaled for 4800x2700 canvas
124+
p.title.text_font_size = "48pt"
125+
p.xaxis.axis_label_text_font_size = "36pt"
126+
p.yaxis.axis_label_text_font_size = "36pt"
127+
p.xaxis.major_label_text_font_size = "28pt"
128+
p.yaxis.major_label_text_font_size = "28pt"
129+
130+
# Background and grid
131+
p.background_fill_color = "#fafafa"
132+
p.grid.grid_line_alpha = 0.3
133+
p.grid.grid_line_dash = "dashed"
134+
p.xgrid.grid_line_color = "#cccccc"
135+
p.ygrid.grid_line_color = "#cccccc"
136+
137+
# Plot X markers (bullish - green)
138+
if x_data:
139+
x_source = ColumnDataSource(data={"col": [d["col"] for d in x_data], "price": [d["price"] for d in x_data]})
140+
p.text(
141+
x="col",
142+
y="price",
143+
text={"value": "X"},
144+
source=x_source,
145+
text_font_size="48pt",
146+
text_color="#2E7D32",
147+
text_align="center",
148+
text_baseline="middle",
149+
text_font_style="bold",
150+
)
151+
152+
# Plot O markers (bearish - red)
153+
if o_data:
154+
o_source = ColumnDataSource(data={"col": [d["col"] for d in o_data], "price": [d["price"] for d in o_data]})
155+
p.text(
156+
x="col",
157+
y="price",
158+
text={"value": "O"},
159+
source=o_source,
160+
text_font_size="48pt",
161+
text_color="#C62828",
162+
text_align="center",
163+
text_baseline="middle",
164+
text_font_style="bold",
165+
)
166+
167+
# Add support trend line (45-degree ascending from low)
168+
all_prices = [d["price"] for d in x_data] + [d["price"] for d in o_data]
169+
if all_prices:
170+
min_price = min(all_prices)
171+
max_price = max(all_prices)
172+
173+
# Find lowest point for support line
174+
support_start_col = 0
175+
support_start_price = min_price - box_size
176+
support_end_col = len(columns) - 1
177+
support_end_price = support_start_price + (support_end_col - support_start_col) * box_size
178+
179+
if support_end_price <= max_price + 2 * box_size:
180+
p.line(
181+
x=[support_start_col, support_end_col],
182+
y=[support_start_price, support_end_price],
183+
line_width=5,
184+
line_color="#306998",
185+
line_dash="solid",
186+
legend_label="Support Trend",
187+
)
188+
189+
# Find highest point for resistance line
190+
resistance_start_col = 0
191+
resistance_start_price = max_price + box_size
192+
resistance_end_col = len(columns) - 1
193+
resistance_end_price = resistance_start_price - (resistance_end_col - resistance_start_col) * box_size
194+
195+
if resistance_end_price >= min_price - 2 * box_size:
196+
p.line(
197+
x=[resistance_start_col, resistance_end_col],
198+
y=[resistance_start_price, resistance_end_price],
199+
line_width=5,
200+
line_color="#FFD43B",
201+
line_dash="solid",
202+
legend_label="Resistance Trend",
203+
)
204+
205+
# Legend styling
206+
p.legend.location = "top_left"
207+
p.legend.label_text_font_size = "28pt"
208+
p.legend.background_fill_alpha = 0.8
209+
p.legend.glyph_height = 30
210+
p.legend.glyph_width = 30
211+
212+
# Save as PNG and HTML (interactive)
213+
export_png(p, filename="plot.png")
214+
save(p, filename="plot.html")

0 commit comments

Comments
 (0)