Skip to content

Commit 602f219

Browse files
Merge remote-tracking branch 'origin/main'
2 parents e1a9065 + 9fd1c0c commit 602f219

2 files changed

Lines changed: 407 additions & 0 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
""" pyplots.ai
2+
kagi-basic: Basic Kagi Chart
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 82/100 | Created: 2026-01-08
5+
"""
6+
7+
import cairosvg
8+
import numpy as np
9+
import pygal
10+
from pygal.style import Style
11+
12+
13+
# Data - Generate synthetic stock price data
14+
np.random.seed(42)
15+
n_days = 300 # More days for richer Kagi pattern
16+
returns = np.random.normal(0.0006, 0.02, n_days)
17+
prices = 100 * np.cumprod(1 + returns)
18+
19+
# Kagi chart calculation with 4% reversal threshold
20+
reversal_pct = 0.04
21+
22+
# Build Kagi chart columns
23+
columns = []
24+
current_direction = "up"
25+
last_high = prices[0]
26+
last_low = prices[0]
27+
prev_swing_high = prices[0]
28+
prev_swing_low = prices[0]
29+
col_idx = 0
30+
31+
current_col = {"x": col_idx, "start": prices[0], "end": prices[0], "type": "yang"}
32+
33+
for price in prices[1:]:
34+
if current_direction == "up":
35+
if price > last_high:
36+
last_high = price
37+
current_col["end"] = price
38+
if price > prev_swing_high:
39+
current_col["type"] = "yang"
40+
elif price < last_high * (1 - reversal_pct):
41+
columns.append(current_col)
42+
prev_swing_high = last_high
43+
col_idx += 1
44+
is_yin = price < prev_swing_low
45+
current_col = {
46+
"x": col_idx,
47+
"start": columns[-1]["end"],
48+
"end": price,
49+
"type": "yin" if is_yin else columns[-1]["type"],
50+
}
51+
current_direction = "down"
52+
last_low = price
53+
else:
54+
if price < last_low:
55+
last_low = price
56+
current_col["end"] = price
57+
if price < prev_swing_low:
58+
current_col["type"] = "yin"
59+
elif price > last_low * (1 + reversal_pct):
60+
columns.append(current_col)
61+
prev_swing_low = last_low
62+
col_idx += 1
63+
is_yang = price > prev_swing_high
64+
current_col = {
65+
"x": col_idx,
66+
"start": columns[-1]["end"],
67+
"end": price,
68+
"type": "yang" if is_yang else columns[-1]["type"],
69+
}
70+
current_direction = "up"
71+
last_high = price
72+
73+
columns.append(current_col)
74+
75+
# Build individual line segments for proper Kagi rendering
76+
# Each segment is a separate 2-point series to avoid polygon connections
77+
yang_segments = [] # List of [(x1,y1), (x2,y2)] for yang
78+
yin_segments = [] # List of [(x1,y1), (x2,y2)] for yin
79+
80+
prev_x = None
81+
prev_y = None
82+
83+
for col in columns:
84+
x = col["x"]
85+
y_start = col["start"]
86+
y_end = col["end"]
87+
seg_type = col["type"]
88+
89+
# Horizontal connector (shoulder/waist) from previous column
90+
if prev_x is not None and prev_x != x:
91+
h_segment = [(prev_x, prev_y), (x, prev_y)]
92+
if seg_type == "yang":
93+
yang_segments.append(h_segment)
94+
else:
95+
yin_segments.append(h_segment)
96+
97+
# Vertical line segment
98+
v_segment = [(x, y_start), (x, y_end)]
99+
if seg_type == "yang":
100+
yang_segments.append(v_segment)
101+
else:
102+
yin_segments.append(v_segment)
103+
104+
prev_x = x
105+
prev_y = y_end
106+
107+
# Colors and stroke widths - large difference for visibility
108+
YANG_COLOR = "#16A34A" # Green for bullish
109+
YIN_COLOR = "#DC2626" # Red for bearish
110+
YANG_WIDTH = 18 # Thick for yang
111+
YIN_WIDTH = 4 # Thin for yin
112+
113+
# Custom style - create colors list with yang color first, yin color second
114+
custom_style = Style(
115+
background="white",
116+
plot_background="white",
117+
foreground="#333333",
118+
foreground_strong="#000000",
119+
foreground_subtle="#999999",
120+
colors=(YANG_COLOR, YIN_COLOR),
121+
title_font_size=64,
122+
label_font_size=42,
123+
major_label_font_size=38,
124+
legend_font_size=38,
125+
value_font_size=30,
126+
guide_stroke_dasharray="4,4",
127+
opacity=1.0,
128+
opacity_hover=1.0,
129+
)
130+
131+
# Create XY chart - disable legend (we'll add manual legend annotation)
132+
chart = pygal.XY(
133+
width=4800,
134+
height=2700,
135+
style=custom_style,
136+
title="kagi-basic · pygal · pyplots.ai",
137+
x_title="Kagi Line Index",
138+
y_title="Price ($)",
139+
show_dots=False,
140+
show_x_guides=False,
141+
show_y_guides=True,
142+
show_legend=False, # Disable native legend - we'll add custom one
143+
stroke=True,
144+
fill=False,
145+
margin=100,
146+
)
147+
148+
# Combine all yang segments into one series (for consistent color)
149+
yang_points = []
150+
for seg in yang_segments:
151+
yang_points.extend(seg)
152+
yang_points.append((None, None)) # Break between segments
153+
154+
# Combine all yin segments into one series
155+
yin_points = []
156+
for seg in yin_segments:
157+
yin_points.extend(seg)
158+
yin_points.append((None, None)) # Break between segments
159+
160+
# Add as two series
161+
chart.add("Yang (Bullish)", yang_points, stroke_style={"width": YANG_WIDTH, "linecap": "round"})
162+
chart.add("Yin (Bearish)", yin_points, stroke_style={"width": YIN_WIDTH, "linecap": "round"})
163+
164+
# Render to SVG and add custom legend with proper line styles
165+
svg_data = chart.render()
166+
svg_str = svg_data.decode("utf-8")
167+
168+
# Add CSS to enforce stroke widths (pygal's stroke_style may not apply correctly)
169+
css_override = f"""
170+
.serie-0 .line {{ stroke-width: {YANG_WIDTH}px !important; stroke: {YANG_COLOR} !important; }}
171+
.serie-1 .line {{ stroke-width: {YIN_WIDTH}px !important; stroke: {YIN_COLOR} !important; }}
172+
"""
173+
svg_str = svg_str.replace("</style>", css_override + "</style>")
174+
175+
# Build manual legend that accurately shows thick vs thin lines
176+
# Position legend in upper right area to avoid axis labels
177+
legend_svg = f"""
178+
<g class="manual-legend" transform="translate(3200, 180)">
179+
<rect x="-20" y="-30" width="1450" height="80" fill="white" fill-opacity="0.9" rx="8"/>
180+
<line x1="0" y1="0" x2="80" y2="0" stroke="{YANG_COLOR}" stroke-width="{YANG_WIDTH}" stroke-linecap="round"/>
181+
<text x="100" y="10" font-size="36" fill="#333333" font-family="Verdana, sans-serif">Yang (Bullish) — Thick</text>
182+
<line x1="700" y1="0" x2="780" y2="0" stroke="{YIN_COLOR}" stroke-width="{YIN_WIDTH}" stroke-linecap="round"/>
183+
<text x="800" y="10" font-size="36" fill="#333333" font-family="Verdana, sans-serif">Yin (Bearish) — Thin</text>
184+
</g>
185+
"""
186+
187+
# Insert legend before closing svg tag
188+
svg_str = svg_str.replace("</svg>", legend_svg + "</svg>")
189+
190+
# Convert to PNG
191+
cairosvg.svg2png(bytestring=svg_str.encode("utf-8"), write_to="plot.png")
192+
193+
# Save HTML version for interactive view
194+
chart.render_to_file("plot.html")

0 commit comments

Comments
 (0)