How to Schedule Daily Stock Analysis Tasks Using Python and the Schedule Library
The ZhuLinsen/daily_stock_analysis repository implements a lightweight single-process scheduler built on the schedule library that registers daily jobs at configurable times, supports immediate execution on startup, and dynamically reloads schedule settings without requiring a process restart.
The daily-stock-analysis system provides a flexible framework for automating equity analysis workflows without external cron jobs or complex orchestration. To schedule daily stock analysis tasks efficiently, the project uses a Python-native scheduling mechanism that handles daily pipeline execution, background task management, and graceful shutdown handling through a cohesive set of modules.
Execution Modes Overview
The system supports three distinct execution modes controlled via CLI flags and configuration settings in main.py:
- One-shot run: Execute the analysis pipeline immediately and exit. Triggered by running
python main.pywithout the--scheduleflag whenconfig.run_immediatelyis enabled. - Scheduled run: Continuously run the scheduler loop, executing the daily task at the configured time. Activated by the
--scheduleCLI flag or settingconfig.schedule_enabled=True(lines 883-883). - Web-UI/API only: Serve the web interface without executing analysis tasks. Triggered by
--serveor--serve-onlyflags (lines 678-698).
How the Scheduler Works
When scheduled mode is activated, main.py instantiates a Scheduler from src/scheduler.py and configures it with the analysis pipeline task and optional background workers.
Daily Task Registration
The scheduler registers the primary analysis task using _configure_daily_task() (lines 30-45), which validates the configured time against a regex pattern and registers the job via:
schedule.every().day.at(schedule_time).do(daily_task)
By default, the system executes at 18:00 (6:00 PM), though this is configurable via the SCHEDULE_TIME environment variable.
Immediate Execution on Startup
The schedule_run_immediately setting (default True) controls whether the daily task runs once immediately upon scheduler startup. When enabled, Scheduler.set_daily_task() calls _safe_run_task() (lines 92-107) before entering the main loop. Disable this behavior using the --no-run-immediately CLI flag, which overrides the configuration setting.
Dynamic Schedule Time Updates
Unlike static cron jobs, the scheduler supports runtime schedule modifications. The _refresh_daily_schedule_if_needed() method (lines 56-71) checks a schedule-time provider function before each loop iteration. This provider reads the latest SCHEDULE_TIME from the persisted .env file or process-level overrides, allowing Web-UI edits to take effect instantly without restarting the process.
Main Loop and Background Tasks
The Scheduler.run() method (lines 73-88) executes three operations on each iteration:
- Refreshes the daily schedule time via the provider.
- Executes pending daily jobs via
self.schedule.run_pending(). - Launches elapsed background tasks via
_run_background_tasks().
The loop sleeps for 30 seconds between iterations, which caps the minimum background task interval at 30 seconds (clamped in add_background_task(), lines 22-27).
Configuring the Schedule
Configuration fields are defined in src/config.py (line 817):
schedule_enabled: bool = False # Enable daily scheduled execution
schedule_time: str = "18:00" # Daily execution time (HH:MM)
schedule_run_immediately: bool = True # Run once immediately on startup
These values can be overridden in the .env file at the project root:
# .env configuration
SCHEDULE_ENABLED=true
SCHEDULE_TIME=07:30
SCHEDULE_RUN_IMMEDIATELY=false
The ConfigManager.read_config_map() method in src/core/config_manager.py provides the runtime configuration reloading capability used by the schedule-time provider.
Command-Line Interface
Start the scheduler using the --schedule flag to force scheduled mode regardless of .env settings:
# Force scheduled mode via CLI
python main.py --schedule
# Disable immediate execution on startup
python main.py --schedule --no-run-immediately
If SCHEDULE_ENABLED is set to true in .env, running python main.py without flags will automatically start the scheduler.
Implementing Custom Background Tasks
The scheduler supports concurrent background tasks running in daemon threads. To add a custom task, define a function and pass it to run_with_schedule() before initialization:
# custom_tasks.py
import logging
import datetime
def health_monitor():
logging.info(f"[HealthCheck] {datetime.datetime.now()} – System operational")
# In main.py, before calling run_with_schedule()
from custom_tasks import health_monitor
background_tasks = [
{
"task": health_monitor,
"interval_seconds": 300, # 5 minutes
"run_immediately": True,
"name": "health_monitor",
},
]
run_with_schedule(
task=scheduled_task,
schedule_time=config.schedule_time,
run_immediately=should_run_immediately,
background_tasks=background_tasks,
schedule_time_provider=schedule_time_provider,
)
Background tasks automatically start in daemon threads and respect the 30-second minimum interval constraint enforced by add_background_task().
Key Implementation Files
| File | Purpose |
|---|---|
src/scheduler.py |
Core Scheduler class, daily task registration, background task handling, and graceful shutdown logic. |
main.py |
CLI entry point that parses --schedule and --no-run-immediately flags, builds the scheduler, and wires configuration providers. |
src/config.py |
Defines schedule_enabled, schedule_time, and schedule_run_immediately configuration fields. |
src/core/config_manager.py |
Provides ConfigManager.read_config_map() for runtime configuration reloading. |
Summary
- The scheduler uses the lightweight
schedulelibrary to run daily stock analysis at configurable times without external cron dependencies. - Three execution modes support one-shot runs, continuous scheduled execution, and web-only serving via CLI flags in
main.py. - Runtime configurability allows changing the schedule time via
.envedits without process restarts, implemented through the_build_schedule_time_provider()mechanism. - Immediate execution can be enabled or disabled via
schedule_run_immediatelyor the--no-run-immediatelyCLI flag. - Background tasks run concurrently in daemon threads with a minimum 30-second interval, managed by
_run_background_tasks().
Frequently Asked Questions
How do I change the daily execution time without restarting the scheduler?
Edit the SCHEDULE_TIME value in your .env file (e.g., SCHEDULE_TIME=09:30). The _refresh_daily_schedule_if_needed() method in src/scheduler.py checks the schedule-time provider on each loop iteration and updates the job automatically. This design allows Web-UI changes to take effect immediately without interrupting the running process.
What is the minimum interval for background tasks?
The minimum interval is 30 seconds, enforced by the add_background_task() method in src/scheduler.py (lines 22-27). The scheduler's main loop sleeps for 30 seconds between iterations, which dictates this limitation. If you specify a smaller interval, the code clamps it to 30 seconds automatically.
How does the system handle graceful shutdown?
The GracefulShutdown class in src/scheduler.py (lines 27-54) registers signal handlers for SIGINT and SIGTERM. When triggered, these set an internal flag that causes the Scheduler.run() loop to exit cleanly after completing the current iteration, ensuring that running analysis tasks or background jobs are not forcibly terminated mid-execution.
Can I run the analysis immediately without waiting for the scheduled time?
Yes. Set SCHEDULE_RUN_IMMEDIATELY=true in your .env file or omit the --no-run-immediately flag when starting with --schedule. This triggers _safe_run_task() during scheduler initialization, executing the full analysis pipeline once before entering the waiting loop for the next scheduled time.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →