1- """ pyplots .ai
1+ """ anyplot .ai
22candlestick-volume: Stock Candlestick Chart with Volume
3- Library: letsplot 4.8.2 | Python 3.13.11
4- Quality: 91 /100 | Created: 2025-12-31
3+ Library: letsplot 4.9.0 | Python 3.13.13
4+ Quality: 90 /100 | Updated: 2026-05-16
55"""
66
7+ import os
8+
79import numpy as np
810import pandas as pd
911from lets_plot import *
1012
1113
1214LetsPlot .setup_html ()
1315
14-
15- # Format volume as human-readable (e.g., 5.0M instead of 5000000)
16- def format_volume (val ):
17- if val >= 1_000_000 :
18- return f"{ val / 1_000_000 :.1f} M"
19- elif val >= 1_000 :
20- return f"{ val / 1_000 :.0f} K"
21- return str (int (val ))
22-
16+ THEME = os .getenv ("ANYPLOT_THEME" , "light" )
17+ PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
18+ ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
19+ INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
20+ INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
2321
2422# Data - 60 trading days of synthetic stock data
2523np .random .seed (42 )
2624n_days = 60
2725dates = pd .date_range ("2024-01-02" , periods = n_days , freq = "B" )
2826
29- # Generate realistic price movement with trend and volatility
30- returns = np .random .normal (0.001 , 0.02 , n_days )
27+ # Generate realistic price movement with more dramatic reversals
28+ returns = np .random .normal (0.001 , 0.025 , n_days )
29+ returns [15 ] = - 0.08 # Dramatic drop
30+ returns [35 ] = 0.06 # Strong bounce
31+ returns [45 ] = - 0.05 # Another reversal
3132close_prices = 150 * np .cumprod (1 + returns )
3233
3334# Generate OHLC from close prices
3435open_prices = np .roll (close_prices , 1 )
3536open_prices [0 ] = 150
36- high_prices = np .maximum (open_prices , close_prices ) * (1 + np .abs (np .random .normal (0 , 0.01 , n_days )))
37- low_prices = np .minimum (open_prices , close_prices ) * (1 - np .abs (np .random .normal (0 , 0.01 , n_days )))
37+ high_prices = np .maximum (open_prices , close_prices ) * (1 + np .abs (np .random .normal (0 , 0.012 , n_days )))
38+ low_prices = np .minimum (open_prices , close_prices ) * (1 - np .abs (np .random .normal (0 , 0.012 , n_days )))
3839
39- # Generate volume with some correlation to price movement
40+ # Generate volume with correlation to price movement
4041base_volume = 5_000_000
4142volatility = np .abs (close_prices - open_prices ) / open_prices
4243volume = base_volume * (1 + volatility * 10 + np .random .uniform (- 0.3 , 0.3 , n_days ))
4344volume = volume .astype (int )
4445
45- # Determine up/down days for coloring (shorter labels to avoid truncation)
46- direction = ["Up Day " if c >= o else "Down Day " for c , o in zip (close_prices , open_prices )]
46+ # Determine up/down days for coloring
47+ direction = ["Up" if c >= o else "Down" for c , o in zip (close_prices , open_prices )]
4748
48- # Create date labels for x-axis (show every 10th trading day )
49+ # Create date labels for x-axis (show every 10 trading days )
4950date_labels = [d .strftime ("%b %d" ) for d in dates ]
5051date_breaks = list (range (0 , n_days , 10 ))
5152date_tick_labels = [date_labels [i ] for i in date_breaks ]
@@ -64,69 +65,67 @@ def format_volume(val):
6465 }
6566)
6667
67- # Colorblind-safe colors (blue for up, orange for down)
68- color_up = "#0077BB"
69- color_down = "#EE7733"
68+ # Colorblind-safe colors (first series Okabe-Ito, second is orange)
69+ color_up = "#009E73"
70+ color_down = "#D55E00"
71+
72+ # Create volume breaks and labels (inline formatting)
73+ vol_min , vol_max = df ["volume" ].min (), df ["volume" ].max ()
74+ vol_breaks = [int (vol_min ), int ((vol_min + vol_max ) / 2 ), int (vol_max )]
75+ vol_labels = [f"{ v / 1_000_000 :.1f} M" if v >= 1_000_000 else f"{ v / 1_000 :.0f} K" for v in vol_breaks ]
76+
77+ # Theme-adaptive styling
78+ anyplot_theme = theme (
79+ plot_background = element_rect (fill = PAGE_BG , color = PAGE_BG ),
80+ panel_background = element_rect (fill = PAGE_BG ),
81+ panel_grid_major = element_line (color = INK_SOFT , size = 0.25 ),
82+ panel_grid_minor = element_blank (),
83+ axis_title = element_text (color = INK , size = 20 ),
84+ axis_text = element_text (color = INK_SOFT , size = 16 ),
85+ axis_line = element_line (color = INK_SOFT , size = 0.3 ),
86+ plot_title = element_text (color = INK , size = 24 ),
87+ legend_background = element_rect (fill = ELEVATED_BG , color = INK_SOFT ),
88+ legend_text = element_text (color = INK_SOFT , size = 16 ),
89+ legend_title = element_text (color = INK , size = 18 ),
90+ )
7091
7192# Create candlestick chart (main pane)
7293candle_plot = (
7394 ggplot (df )
74- # Wicks (high-low lines)
75- + geom_segment (aes (x = "date_idx" , xend = "date_idx" , y = "low" , yend = "high" , color = "direction" ), size = 1.0 )
95+ # Wicks (high-low lines) - thicker for visibility
96+ + geom_segment (aes (x = "date_idx" , xend = "date_idx" , y = "low" , yend = "high" , color = "direction" ), size = 1.5 )
7697 # Bodies (open-close rectangles)
77- + geom_segment (aes (x = "date_idx" , xend = "date_idx" , y = "open" , yend = "close" , color = "direction" ), size = 5 .0 )
78- + scale_color_manual (values = {"Up Day " : color_up , "Down Day " : color_down }, name = "Direction" )
98+ + geom_segment (aes (x = "date_idx" , xend = "date_idx" , y = "open" , yend = "close" , color = "direction" ), size = 6 .0 )
99+ + scale_color_manual (values = {"Up" : color_up , "Down" : color_down }, name = "Direction" )
79100 + scale_x_continuous (breaks = date_breaks , labels = date_tick_labels )
80- + labs (title = "candlestick-volume · letsplot · pyplots.ai" , y = "Price ($)" , x = "" )
81- # Enable crosshair cursor for precise reading
82- + coord_cartesian ()
83- + theme_minimal ()
101+ + labs (title = "Stock Trading · candlestick-volume · letsplot · anyplot.ai" , y = "Price ($)" , x = "" )
102+ + anyplot_theme
84103 + theme (
85- plot_title = element_text (size = 24 ),
86- axis_title_y = element_text (size = 20 ),
87- axis_text_y = element_text (size = 16 ),
88104 axis_text_x = element_blank (),
89- legend_position = [0.5 , 0.98 ],
105+ legend_position = [0.5 , 0.95 ],
90106 legend_justification = [0.5 , 1.0 ],
91107 legend_direction = "horizontal" ,
92- legend_title = element_text (size = 18 ),
93- legend_text = element_text (size = 16 ),
94- panel_grid_major = element_line (color = "#E5E7EB" , size = 0.5 ),
95- panel_grid_minor = element_blank (),
96- plot_margin = [40 , 20 , 5 , 10 ],
108+ plot_margin = [40 , 20 , 2 , 10 ],
97109 )
98110 + ggsize (1600 , 630 )
99111)
100112
101- # Create volume breaks and labels for human-readable format
102- vol_min , vol_max = df ["volume" ].min (), df ["volume" ].max ()
103- vol_breaks = [int (vol_min ), int ((vol_min + vol_max ) / 2 ), int (vol_max )]
104- vol_labels = [format_volume (v ) for v in vol_breaks ]
105-
106113# Volume chart (lower pane)
107114volume_plot = (
108115 ggplot (df )
109116 + geom_bar (aes (x = "date_idx" , y = "volume" , fill = "direction" ), stat = "identity" , width = 0.8 )
110- + scale_fill_manual (values = {"Up Day " : color_up , "Down Day " : color_down }, name = "Direction" )
117+ + scale_fill_manual (values = {"Up" : color_up , "Down" : color_down }, name = "Direction" )
111118 + scale_x_continuous (breaks = date_breaks , labels = date_tick_labels )
112119 + scale_y_continuous (breaks = vol_breaks , labels = vol_labels )
113120 + labs (x = "Date (2024)" , y = "Volume (shares)" )
114- # Enable crosshair cursor for precise reading (interactive HTML)
115- + coord_cartesian ()
116- + theme_minimal ()
117- + theme (
118- axis_title = element_text (size = 20 ),
119- axis_text = element_text (size = 16 ),
120- legend_position = "none" ,
121- panel_grid_major = element_line (color = "#E5E7EB" , size = 0.5 ),
122- panel_grid_minor = element_blank (),
123- )
121+ + anyplot_theme
122+ + theme (legend_position = "none" , plot_margin = [2 , 20 , 10 , 10 ])
124123 + ggsize (1600 , 270 )
125124)
126125
127- # Use gggrid for dual-pane layout (replaces deprecated GGBunch)
126+ # Use gggrid for dual-pane layout with tighter spacing
128127combined = gggrid ([candle_plot , volume_plot ], ncol = 1 , heights = [0.7 , 0.3 ])
129128
130- # Save outputs (path='' ensures files are saved in current directory)
131- ggsave (combined , "plot.png" , scale = 3 , path = "." )
132- ggsave (combined , "plot.html" , path = "." )
129+ # Save outputs
130+ ggsave (combined , f "plot- { THEME } .png" , scale = 3 , path = "." )
131+ ggsave (combined , f "plot- { THEME } .html" , path = "." )
0 commit comments