Skill: tltby12341/qc-backtest-master (version 1.0.0)

--- name: qc-backtest-master description: QuantConnect automated backtest pipeline — submit local strategies, compile, execute, monitor with early-stop, and download results in one command. version: 1.0.0 metadata: openclaw: requires: env: - QC_USER_ID - QC_API_TOKEN - QC_PROJECT_ID bins: - python3 primaryEnv: QC_API_TOKEN emoji: "\U0001F680" --- # QC Backtest Master Automate the full QuantConnect backtest lifecycle from local code to cloud results, without ever opening the web UI. ## What this skill does When the user asks to backtest a strategy, run a backtest, or submit code to QuantConnect, follow this workflow: ### 1. Submit & Compile Upload the user's local `.py` strategy file to a QuantConnect cloud project, replacing any existing `main.py`. Trigger compilation and report any errors with line numbers. ```bash python3 submit_backtest.py \ --main-file <path_to_strategy.py> \ --name "<backtest_name>" \ --project-id $QC_PROJECT_ID ``` If the strategy has auxiliary data files (`.json`, `.txt`, `.csv`), upload those too. ### 2. Monitor with Early-Stop Poll backtest progress in real time. If drawdown exceeds the user's threshold after 20% progress, automatically delete the backtest to save compute time. ```bash python3 monitor_backtest.py <backtest_id> \ --max-dd <threshold> \ --check-after-progress 0.20 \ --timeout 3600 ``` **Exit codes:** | Code | Meaning | |------|---------| | `0` | Backtest completed normally | | `1` | Runtime error (print full stack trace) | | `2` | Timeout | | `3` | Early stop: drawdown exceeded threshold, backtest deleted | ### 3. Download Results Fetch the complete backtest statistics (JSON) and all order records (CSV, paginated at 100 per call to bypass QC's limit). ```bash python3 get_results.py <backtest_id> --output-dir ./results/<name> ``` **Output files:** - `<backtest_id>_result.json` — Full statistics, charts, runtime data - `<backtest_id>_orders.csv` — All orders with symbol, direction, quantity, fill price, fees, timestamps ### 4. Full Pipeline (One Command) Run the entire submit-compile-monitor-download pipeline: ```bash python3 run_workflow.py \ --main-file <path_to_strategy.py> \ --name "<backtest_name>" \ --max-dd 40 ``` ### 5. List Historical Backtests ```bash python3 list_backtests.py --limit 10 ``` ## QC API Client The `qc_api/client.py` module wraps the QuantConnect REST API v2: - `create_backtest(project_id, name, compile_id)` — Start a backtest - `read_backtest(project_id, backtest_id)` — Get status and results - `read_backtest_orders(project_id, backtest_id, start, end)` — Paginated order fetch - `delete_backtest(project_id, backtest_id)` — Delete (used for early-stop) - `update_project_file(project_id, name, content)` — Upload code - `compile_project(project_id)` — Trigger compilation Credentials are read from environment variables `QC_USER_ID` and `QC_API_TOKEN`. ## Rules - **Entry file**: Local strategy files are uploaded as `main.py`. Old `Main.py` is auto-deleted to prevent conflicts. Do not manually rename files on the QC cloud project. - **One backtest at a time**: QC free tier allows only 1 concurrent backtest per project. Never submit a second backtest while one is running — it will silently fail or queue. - **Logs are not available via API**: QC REST API v2 does not expose `self.log()` output. View logs in the QC web UI manually. - **Early-stop threshold**: Accepts both fraction (`0.40`) and percentage (`40`) formats. Early-stop **permanently deletes** the backtest — it cannot be recovered. - **Environment variables `QC_USER_ID`, `QC_API_TOKEN`, and `QC_PROJECT_ID` must be set.** The skill will raise a clear error if any are missing. - **Do not modify cloud project files outside this skill.** The skill assumes it owns `main.py` and will overwrite any manual changes on the next submission.

path: get_results.py
content: import sys
import json
import csv
import argparse
import os
from qc_api.client import QCApiClient
from qc_api.config import get_default_project_id

ORDER_TYPE = {0: "Market", 1: "Limit", 2: "StopMarket", 3: "StopLimit", 4: "MarketOnOpen", 5: "MarketOnClose"}
ORDER_STATUS = {0: "New", 1: "Submitted", 2: "PartiallyFilled", 3: "Filled", 4: "Cancelled", 5: "Invalid"}

def main():
    parser = argparse.ArgumentParser(description="Get results of a completed backtest")
    parser.add_argument("backtest_id", help="Backtest ID")
    parser.add_argument("--project-id", type=int, default=get_default_project_id(), help="QuantConnect Project ID")
    parser.add_argument("--output-dir", default=".", help="Directory to save results")

    args = parser.parse_args()
    os.makedirs(args.output_dir, exist_ok=True)
    client = QCApiClient()

    print(f"Fetching results for Backtest {args.backtest_id}...")

    try:
        # 1. Get main backtest object
        resp = client.read_backtest(args.project_id, args.backtest_id)
        result = resp.get('backtest', resp)

        # Save full stats JSON
        json_path = os.path.join(args.output_dir, f"{args.backtest_id}_result.json")
        with open(json_path, 'w') as f:
            json.dump(resp, f, indent=2)
        print(f"Full result saved to {json_path}")

        # Check for runtime error
        if result.get('error'):
            print(f"\nBacktest ended with error: {result['error']}")

        # 2. Key Statistics
        stats = result.get('statistics', {})
        if stats:
            print("\nKey Statistics:")
            for k in ["Total Orders", "Net Profit", "Sharpe Ratio", "Drawdown", "Win Rate", "Total Fees"]:
                if k in stats:
                    print(f"  - {k}: {stats[k]}")

        # 3. Orders via dedicated endpoint
        all_orders = []
        start_idx = 0
        limit = 100
        while True:
            orders_resp = client.read_backtest_orders(args.project_id, args.backtest_id, start=start_idx, end=start_idx + limit)
            batch = orders_resp.get('orders', [])
            if not batch:
                break
            all_orders.extend(batch)
            # Break if fewer than limit items returned (last page)
            if len(batch) < limit:
                break
            start_idx += limit

        if all_orders:
            csv_path = os.path.join(args.output_dir, f"{args.backtest_id}_orders.csv")
            rows = []
            for o in all_orders:
                symbol = o.get('symbol', {}).get('value', '')
                fill_event = next((e for e in o.get('events', []) if e.get('status') == 'filled'), None)
                rows.append({
                    'orderId':     o.get('id'),
                    'symbol':      symbol,
                    'type':        ORDER_TYPE.get(o.get('type'), o.get('type')),
                    'direction':   'buy' if o.get('direction') == 0 else 'sell',
                    'quantity':    o.get('quantity'),
                    'fillPrice':   fill_event['fillPrice'] if fill_event else '',
                    'fillQty':     fill_event['fillQuantity'] if fill_event else '',
                    'fee':         fill_event.get('orderFeeAmount', '') if fill_event else '',
                    'status':      ORDER_STATUS.get(o.get('status'), o.get('status')),
                    'submitTime':  o.get('time'),
                    'fillTime':    o.get('lastFillTime'),
                    'tag':         o.get('tag'),
                })
            with open(csv_path, 'w', newline='') as csvfile:
                writer = csv.DictWriter(csvfile, fieldnames=rows[0].keys())
                writer.writeheader()
                writer.writerows(rows)
            print(f"Orders ({len(rows)}) saved to {csv_path}")
        else:
            print("No orders in this backtest.")

        # 4. Logs -- not available via QC REST API v2
        # Logs can only be downloaded from the QuantConnect web UI (Backtest > Logs tab).
        print("Logs: not available via API. Download from QC web UI -> Backtest -> Logs tab.")

    except Exception as e:
        print(f"Failed to fetch results: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()


path: monitor_backtest.py
content: import sys
import time
import argparse
from qc_api.client import QCApiClient
from qc_api.config import get_default_project_id

TIMEOUT_SECONDS = 3600  # 1 hour

# Exit codes
EXIT_SUCCESS   = 0   # Backtest completed normally
EXIT_ERROR     = 1   # Runtime error in backtest
EXIT_TIMEOUT   = 2   # Monitoring timed out
EXIT_EARLY_STOP = 3  # Threshold exceeded — deleted and stopped early


def _parse_pct(raw):
    """Parse '59.700%' or '0.597' -> float 0.597. Returns None if unparseable."""
    if raw is None:
        return None
    s = str(raw).strip().rstrip('%')
    try:
        val = float(s)
        # QC returns drawdown as e.g. "59.700" (already a percentage), not 0.597
        return val / 100.0 if val > 1.0 else val
    except ValueError:
        return None


def _parse_equity(raw):
    """Parse '$2,881.20' or '-$7,010.04' -> float. Returns None if unparseable."""
    if raw is None:
        return None
    s = str(raw).strip().replace(',', '').replace('$', '')
    try:
        return float(s)
    except ValueError:
        return None


def main():
    parser = argparse.ArgumentParser(
        description="Monitor a running backtest with optional early-stop on threshold breach"
    )
    parser.add_argument("backtest_id", help="Backtest ID to monitor")
    parser.add_argument("--project-id", type=int, default=get_default_project_id(),
                        help="QuantConnect Project ID")
    parser.add_argument("--timeout", type=int, default=TIMEOUT_SECONDS,
                        help="Max monitoring time in seconds (default 3600)")
    parser.add_argument("--max-dd", type=float, default=None,
                        help=(
                            "Early-stop threshold: delete backtest if drawdown exceeds this value. "
                            "Accepts fraction (0.40) or percentage (40). "
                            "Example: --max-dd 0.40 or --max-dd 40"
                        ))
    parser.add_argument("--check-after-progress", type=float, default=0.20,
                        help=(
                            "Only start threshold checks after this fraction of progress "
                            "(default 0.20 = 20%%). Avoids false positives from early volatility."
                        ))

    args = parser.parse_args()

    # Normalise max-dd: accept both 0.40 and 40 as "40%"
    max_dd = None
    if args.max_dd is not None:
        max_dd = args.max_dd if args.max_dd <= 1.0 else args.max_dd / 100.0

    client = QCApiClient()
    print(f"Monitoring Backtest {args.backtest_id}...")
    if max_dd is not None:
        print(f"   Early-stop: DD > {max_dd*100:.0f}%  "
              f"(active after {args.check_after_progress*100:.0f}% progress)")

    start_time = time.time()
    had_error = False
    peak_equity = 0.0  # high-water mark for live drawdown calculation

    while True:
        elapsed = time.time() - start_time
        if elapsed > args.timeout:
            print(f"\nTimeout: backtest did not finish within {args.timeout}s.")
            sys.exit(EXIT_TIMEOUT)

        try:
            resp = client.read_backtest(args.project_id, args.backtest_id)
            bt = resp.get('backtest', resp)
            state = bt.get('progress', 0)

            # Progress bar
            bar_length = 30
            filled = int(bar_length * state)
            bar = '=' * filled + '-' * (bar_length - filled)
            sys.stdout.write(f"\rProgress: |{bar}| {state*100:.1f}%")
            sys.stdout.flush()

            # Runtime error check
            error = bt.get('error')
            if error:
                print(f"\nRuntime Error:\n{error}")
                had_error = True
                break

            # Completion check
            if state >= 1.0:
                print("\nBacktest Completed!")
                break

            # -- Live equity tracking for real-time drawdown --
            runtime_stats = bt.get('runtimeStatistics', {})
            current_equity = _parse_equity(runtime_stats.get('Equity'))
            if current_equity is not None and current_equity > peak_equity:
                peak_equity = current_equity

            live_dd = None
            if peak_equity > 0 and current_equity is not None:
                live_dd = (peak_equity - current_equity) / peak_equity

            # -- Early-stop threshold check --
            if max_dd is not None and state >= args.check_after_progress:
                # Prefer live drawdown from equity tracking; fall back to statistics
                dd = live_dd
                if dd is None:
                    stats = bt.get('statistics', {})
                    dd = _parse_pct(stats.get('Drawdown'))

                if dd is not None and dd > max_dd:
                    print(
                        f"\nEarly Stop triggered at {state*100:.1f}% progress: "
                        f"Drawdown {dd*100:.1f}% > threshold {max_dd*100:.0f}%"
                    )
                    print(f"   Deleting backtest {args.backtest_id} via API...")
                    try:
                        client.delete_backtest(args.project_id, args.backtest_id)
                        print("   Backtest deleted.")
                    except Exception as del_err:
                        print(f"   Delete failed (backtest may have already finished): {del_err}")
                    print(f"OUTPUT_EARLY_STOP_DD:{dd*100:.2f}")
                    sys.exit(EXIT_EARLY_STOP)

        except Exception as e:
            print(f"\nMonitoring Error: {e}")
            time.sleep(5)

        time.sleep(2)

    if had_error:
        sys.exit(EXIT_ERROR)


if __name__ == "__main__":
    main()


path: qc_api/client.py
content: import requests
import json
import time
import hashlib
import base64
import logging
from .config import get_credentials

logger = logging.getLogger("qc_api")

# Retry configuration
MAX_RETRIES = 3
RETRY_BACKOFF_BASE = 2  # Exponential backoff base (seconds)
RETRYABLE_STATUS_CODES = {500, 502, 503, 504, 429}


class QCApiClient:
    def __init__(self):
        self.user_id, self.api_token = get_credentials()
        self.base_url = "https://www.quantconnect.com/api/v2"

    def _request(self, method, endpoint, data=None):
        url = f"{self.base_url}/{endpoint}"

        last_exception = None
        for attempt in range(MAX_RETRIES):
            ts = str(int(time.time()))
            token_hash = hashlib.sha256(
                f"{self.api_token}:{ts}".encode('utf-8')
            ).hexdigest()
            auth_str = f"{self.user_id}:{token_hash}"
            base64_auth = base64.b64encode(auth_str.encode('ascii')).decode('ascii')

            headers = {
                "Accept": "application/json",
                "Timestamp": ts,
                "Authorization": f"Basic {base64_auth}",
            }

            try:
                if method.lower() == 'get':
                    response = requests.get(url, params=data, headers=headers, timeout=30)
                elif method.lower() == 'post':
                    response = requests.post(url, data=data, headers=headers, timeout=60)
                else:
                    raise ValueError(f"Unsupported method: {method}")

                # Retryable HTTP status codes
                if response.status_code in RETRYABLE_STATUS_CODES:
                    wait = RETRY_BACKOFF_BASE ** attempt
                    logger.warning(
                        f"API {endpoint} returned {response.status_code}, "
                        f"retrying in {wait}s ({attempt + 1}/{MAX_RETRIES})"
                    )
                    time.sleep(wait)
                    last_exception = requests.exceptions.HTTPError(
                        f"{response.status_code}: {response.text[:200]}"
                    )
                    continue

                if response.status_code != 200:
                    logger.error(f"API Error {response.status_code}: {response.text[:300]}")

                response.raise_for_status()
                return response.json()

            except requests.exceptions.ConnectionError as e:
                wait = RETRY_BACKOFF_BASE ** attempt
                logger.warning(
                    f"API {endpoint} connection failed, "
                    f"retrying in {wait}s ({attempt + 1}/{MAX_RETRIES}): {e}"
                )
                time.sleep(wait)
                last_exception = e
            except requests.exceptions.Timeout as e:
                wait = RETRY_BACKOFF_BASE ** attempt
                logger.warning(
                    f"API {endpoint} timeout, "
                    f"retrying in {wait}s ({attempt + 1}/{MAX_RETRIES}): {e}"
                )
                time.sleep(wait)
                last_exception = e
            except requests.exceptions.RequestException as e:
                # Non-retryable errors (4xx, etc.) — raise immediately
                logger.error(f"API Request Error ({endpoint}): {e}")
                raise

        # Retries exhausted
        logger.error(f"API {endpoint} still failed after {MAX_RETRIES} retries")
        raise last_exception or RuntimeError(f"API {endpoint} failed after {MAX_RETRIES} retries")

    def read_project(self, project_id):
        """Test connection by reading project"""
        return self._request('get', 'projects/read', data={'projectId': project_id})

    def update_project_files(self, project_id, files_list):
        """
        Update text files in a project.
        files_list: list of {'name': 'main.py', 'content': '...'}
        QC API requires one request per file with individual name/content fields.
        """
        results = []
        for f in files_list:
            payload = {
                'projectId': project_id,
                'name': f['name'],
                'content': f['content'],
            }
            result = self._request('post', 'files/update', data=payload)
            if not result.get('success'):
                raise RuntimeError(f"Failed to update {f['name']}: {result.get('errors')}")
            results.append(result)
        return results

    def delete_file(self, project_id, file_name):
        """Delete a file from the project"""
        return self._request('post', 'files/delete', data={
            'projectId': project_id,
            'name': file_name
        })

    def create_compile(self, project_id):
        """Trigger a compilation"""
        return self._request('post', 'compile/create', data={'projectId': project_id})

    def read_compile(self, project_id, compile_id):
        """Check compilation status"""
        return self._request('get', 'compile/read', data={'projectId': project_id, 'compileId': compile_id})

    def create_backtest(self, project_id, compile_id, backtest_name):
        """Trigger a backtest"""
        return self._request('post', 'backtests/create', data={
            'projectId': project_id,
            'compileId': compile_id,
            'backtestName': backtest_name
        })

    def read_backtest(self, project_id, backtest_id):
        """Check backtest status and get results"""
        return self._request('get', 'backtests/read', data={
            'projectId': project_id,
            'backtestId': backtest_id
        })

    def read_backtest_orders(self, project_id, backtest_id, start=0, end=100):
        """Read orders from a completed backtest"""
        return self._request('get', 'backtests/orders/read', data={
            'projectId': project_id,
            'backtestId': backtest_id,
            'start': start,
            'end': end,
        })

    def list_backtests(self, project_id):
        """List all backtests for a project"""
        return self._request('get', 'backtests/read', data={'projectId': project_id})

    def delete_backtest(self, project_id, backtest_id):
        """Delete (abort) a running or completed backtest. Does NOT delete the project."""
        return self._request('post', 'backtests/delete', data={
            'projectId': project_id,
            'backtestId': backtest_id,
        })


path: qc_api/config.py
content: import os
import re


def _load_dotenv():
    """Load environment variables from a .env file in the project root (does not overwrite existing vars)."""
    # Search upward from the current file's directory for a .env file
    search_dir = os.path.dirname(os.path.abspath(__file__))
    for _ in range(6):  # Search up to 6 levels
        env_path = os.path.join(search_dir, ".env")
        if os.path.exists(env_path):
            with open(env_path) as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith('#') or '=' not in line:
                        continue
                    key, _, value = line.partition('=')
                    key = key.strip()
                    value = value.strip().strip('"').strip("'")
                    if key and key not in os.environ:
                        os.environ[key] = value
            return
        parent = os.path.dirname(search_dir)
        if parent == search_dir:
            break
        search_dir = parent


def get_credentials():
    """
    Read QuantConnect API credentials.
    Priority: environment variables > project .env file > ~/.lean/credentials file
    """
    # 0. Attempt to load from .env file
    _load_dotenv()

    # 1. Environment variables (including those loaded from .env)
    user_id = os.environ.get("QC_USER_ID")
    api_token = os.environ.get("QC_API_TOKEN")
    if user_id and api_token:
        return user_id, api_token

    # 2. Fallback to ~/.lean/credentials file
    lean_cred_path = os.path.expanduser("~/.lean/credentials")
    if os.path.exists(lean_cred_path):
        try:
            with open(lean_cred_path) as f:
                content = f.read()
            uid_match = re.search(r'user-id\s*=\s*(\S+)', content)
            token_match = re.search(r'api-token\s*=\s*(\S+)', content)
            if uid_match and token_match:
                return uid_match.group(1), token_match.group(1)
        except Exception:
            pass

    raise ValueError(
        "QuantConnect credentials not configured. Set environment variables:\n"
        "  export QC_USER_ID=your_user_id\n"
        "  export QC_API_TOKEN=your_api_token\n"
        "Or configure user-id and api-token in ~/.lean/credentials"
    )


def get_default_project_id():
    """
    Returns the default project ID from the QC_PROJECT_ID environment variable.
    Raises ValueError if not set.
    """
    project_id = os.environ.get("QC_PROJECT_ID")
    if not project_id:
        raise ValueError(
            "QC_PROJECT_ID not set. Please set it:\n"
            "  export QC_PROJECT_ID=your_project_id"
        )
    return int(project_id)


path: requirements.txt
content: requests>=2.28.0


path: run_workflow.py
content: import sys
import subprocess
import os
import argparse
import time
from qc_api.config import get_default_project_id

# Monitor exit codes (mirrors monitor_backtest.py)
EXIT_MONITOR_SUCCESS    = 0
EXIT_MONITOR_ERROR      = 1
EXIT_MONITOR_TIMEOUT    = 2
EXIT_MONITOR_EARLY_STOP = 3


def run_script(script_path, args):
    """
    Runs a python script and streams output line-by-line.
    Returns: (returncode, full_output_lines, captured_id, captured_early_stop_dd)
    """
    python_cmd = sys.executable
    cmd = [python_cmd, script_path] + args

    print(f"\nRunning: {os.path.basename(script_path)} {' '.join(args)}")

    try:
        process = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            bufsize=1,
            universal_newlines=True,
        )

        captured_id = None
        captured_early_stop_dd = None
        full_output = []

        while True:
            line = process.stdout.readline()
            if not line and process.poll() is not None:
                break
            if line:
                stripped = line.strip()
                print(stripped)
                full_output.append(stripped)

                if "OUTPUT_BACKTEST_ID:" in stripped:
                    captured_id = stripped.split("OUTPUT_BACKTEST_ID:")[1].strip()
                if "OUTPUT_EARLY_STOP_DD:" in stripped:
                    captured_early_stop_dd = stripped.split("OUTPUT_EARLY_STOP_DD:")[1].strip()

        returncode = process.poll()
        return returncode, full_output, captured_id, captured_early_stop_dd

    except Exception as e:
        print(f"Failed to run script: {e}")
        return -1, [], None, None


def main():
    default_project_id = str(get_default_project_id())

    parser = argparse.ArgumentParser(description="Full Workflow: Submit -> Monitor -> Get Results")
    parser.add_argument("--main-file", required=True, help="Path to the main strategy .py file")
    parser.add_argument("--project-id", type=str, default=default_project_id, help="QuantConnect Project ID")
    parser.add_argument("--name", help="Backtest Name")
    parser.add_argument("--max-dd", type=float, default=None,
                        help=(
                            "Early-stop threshold: delete backtest if drawdown exceeds this value "
                            "mid-run. Accepts fraction (0.40) or percentage (40). "
                            "Example: --max-dd 40"
                        ))
    parser.add_argument("--check-after-progress", type=float, default=0.20,
                        help=(
                            "Start early-stop checks only after this fraction of progress "
                            "(default 0.20 = 20%%). Avoids false positives from early volatility."
                        ))

    args = parser.parse_args()
    base_dir = os.path.dirname(os.path.abspath(__file__))

    # -- 1. Submit --
    submit_script = os.path.join(base_dir, "submit_backtest.py")
    submit_args = ["--main-file", args.main_file, "--project-id", args.project_id]
    if args.name:
        submit_args.extend(["--name", args.name])

    rc, _, backtest_id, _ = run_script(submit_script, submit_args)
    if rc != 0 or not backtest_id:
        print("Submission failed or Backtest ID not found.")
        sys.exit(1)

    print(f"\nBacktest ID Captured: {backtest_id}")
    time.sleep(2)

    # -- 2. Monitor --
    monitor_script = os.path.join(base_dir, "monitor_backtest.py")
    monitor_args = [backtest_id, "--project-id", args.project_id]
    if args.max_dd is not None:
        monitor_args.extend(["--max-dd", str(args.max_dd)])
        monitor_args.extend(["--check-after-progress", str(args.check_after_progress)])

    rc, _, _, early_stop_dd = run_script(monitor_script, monitor_args)

    if rc == EXIT_MONITOR_ERROR:
        print("Backtest ended with a runtime error. Skipping result download.")
        sys.exit(1)

    if rc == EXIT_MONITOR_TIMEOUT:
        print("Monitoring timed out. Skipping result download.")
        sys.exit(2)

    if rc == EXIT_MONITOR_EARLY_STOP:
        dd_str = f"{early_stop_dd}%" if early_stop_dd else "unknown"
        print(f"\nWorkflow stopped early: drawdown ({dd_str}) exceeded threshold.")
        print("   Backtest has been deleted from QC. No results to download.")
        print("   -> Adjust strategy and resubmit.")
        sys.exit(3)

    # -- 3. Get Results --
    get_results_script = os.path.join(base_dir, "get_results.py")
    get_results_args = [backtest_id, "--project-id", args.project_id, "--output-dir", os.getcwd()]

    rc, _, _, _ = run_script(get_results_script, get_results_args)
    if rc == 0:
        print("\nWorkflow Completed Successfully!")
    else:
        print("\nFailed to retrieve results.")


if __name__ == "__main__":
    main()


path: submit_backtest.py
content: import os
import sys
import argparse
import time
from qc_api.client import QCApiClient
from qc_api.config import get_default_project_id

def get_project_files(main_file_path):
    """
    Reads the specified main file and other relevant data files in the same directory.
    Returns a list of dicts: [{'name': 'main.py', 'content': '...'}]
    """
    if not os.path.isfile(main_file_path):
        print(f"Error: {main_file_path} is not a valid file.")
        return []

    files_list = []
    base_dir = os.path.dirname(main_file_path)

    # 1. Add the main strategy file (renamed to main.py for QuantConnect)
    try:
        with open(main_file_path, 'r', encoding='utf-8') as f:
            content = f.read()
        files_list.append({'name': 'main.py', 'content': content})
        print(f"Added main file: {os.path.basename(main_file_path)} -> main.py")
    except Exception as e:
        print(f"Failed to read main file: {e}")
        return []

    # 2. Optionally add other relevant files in the same directory (excluding .py files to avoid conflicts)
    valid_data_extensions = {'.json', '.txt', '.csv'}
    for file in os.listdir(base_dir):
        ext = os.path.splitext(file)[1]
        if ext in valid_data_extensions:
            full_path = os.path.join(base_dir, file)
            try:
                with open(full_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                files_list.append({'name': file, 'content': content})
                print(f"Added data file: {file}")
            except Exception as e:
                print(f"Skipping data file {file}: {e}")

    return files_list

def main():
    parser = argparse.ArgumentParser(description="Submit a specific strategy file to QuantConnect")
    parser.add_argument("--main-file", required=True, help="Path to the main strategy .py file")
    parser.add_argument("--project-id", type=int, default=get_default_project_id(), help="QuantConnect Project ID")
    parser.add_argument("--name", help="Backtest Name", default=None)

    args = parser.parse_args()

    client = QCApiClient()
    print(f"Initializing Backtest for Project ID: {args.project_id}")

    # 1. Update Files
    print("Preparing files for sync...")
    files = get_project_files(args.main_file)
    if not files:
        print("No valid files found to sync!")
        sys.exit(1)

    # Delete old Main.py (capital M) to avoid running stale code
    print("Deleting old Main.py from cloud project...")
    try:
        client.delete_file(args.project_id, 'Main.py')
        print("Old Main.py deleted.")
    except Exception as e:
        print(f"Could not delete Main.py (may not exist): {e}")

    print("Uploading new files...")
    client.update_project_files(args.project_id, files)
    print(f"Synced {len(files)} files to QuantConnect.")

    # 2. Compile
    print("Compiling project...")
    compile_result = client.create_compile(args.project_id)
    compile_id = compile_result['compileId']

    # Wait for compilation
    while True:
        status = client.read_compile(args.project_id, compile_id)
        state = status['state']

        if state == "BuildSuccess":
            print("Compilation Successful")
            break
        elif state == "BuildError":
            print("Compilation Failed!")
            if 'logs' in status and status['logs']:
                for log in status['logs']:
                    print(f"  {log}")
            sys.exit(1)

        time.sleep(1)

    # 3. Create Backtest
    # Default name to filename without extension + timestamp
    default_name = f"{os.path.splitext(os.path.basename(args.main_file))[0]}_{int(time.time())}"
    bt_name = args.name if args.name else default_name
    print(f"Starting Backtest: {bt_name}")

    bt_result = client.create_backtest(args.project_id, compile_id, bt_name)

    bt_obj = bt_result.get('backtest', bt_result)
    if 'backtestId' in bt_obj:
        backtest_id = bt_obj['backtestId']
        print(f"Backtest Created. ID: {backtest_id}")
        # Used by run_workflow.py to capture ID
        print(f"OUTPUT_BACKTEST_ID:{backtest_id}")
    else:
        print(f"Failed to create backtest: {bt_result.get('errors', ['Unknown error'])}")
        sys.exit(1)


if __name__ == "__main__":
    main()


path: verify_connection.py
content: import sys
import os

# Add current directory to path to allow importing qc_api
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

try:
    from qc_api.client import QCApiClient
    from qc_api.config import get_default_project_id

    client = QCApiClient()
    project_id = get_default_project_id()

    print(f"Testing connection for Project ID: {project_id}")
    project_info = client.read_project(project_id)

    if project_info and 'success' in project_info and project_info['success']:
        print("API Connection Successful!")
        print(f"Project Name: {project_info.get('projects', [{}])[0].get('name', 'Unknown')}")
    else:
        print(f"API Request Failed: {project_info}")

except Exception as e:
    print(f"Error during verification: {e}")


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)