v1

Signals API

Pull our Swing Trade and Swing Elite trading signals straight into your own bot over plain HTTP + JSON. You decide what to do with them and trade your own account.

Introduction

The Signals API is a read-only REST API. Every request returns JSON. The same signals are served to every subscriber on your plan — the API never touches your money, never places trades, and is not personalized to your circumstances. It tells you which stocks the strategy would enter today and the exact rule to exit; your bot does the trading.

Base URL

https://picckles.com/api/v1

Authentication

Authenticate every request with your API key, either as a bearer token or the x-api-key header. Keys are shown once when issued — store them securely and never expose them in client-side code.

Authorization header
Authorization: Bearer pk_harvest_xxxxxxxxxxxxxxxxxxxx
or x-api-key header
x-api-key: pk_harvest_xxxxxxxxxxxxxxxxxxxx

A missing or invalid key returns 401. Get a free Seed key or subscribe on the pricing page.

Quickstart

Fetch today’s Swing Trade signals:

curl
curl https://picckles.com/api/v1/signals/double7-guarded \
  -H "Authorization: Bearer pk_harvest_xxxxxxxxxxxxxxxxxxxx"

Endpoints

GET/api/v1/signals/{strategy}
Returns one strategy feed — today’s BUY signals. {strategy} is double7-guarded (Swing Trade), or swing-elite. Returns 403 if your plan doesn’t include that strategy.
POST/api/v1/signals/{strategy}/exits
The SELL side. Send the positions your bot holds; get a SELL/HOLD decision per position. Read-only — it uses POST only because it takes a request body (your positions). See Exit signals.
How to read the feed: the buy feed lists only the stocks with a signal today. A stock that isn’t in the list has no action — don’t buy it. You won’t get a row for every ticker, only the actionable ones.

Those two are the complete developer API. Account, keys, plans and billing are managed on the website (the pricing and dashboard pages), not through the API.

entriesis always ranked and capped to match the live product this feed mirrors, up to that product's own concurrent-position slot count (6 for both feeds). More names can qualify on a given day than that (see totalQualifiers below); buying every qualifier instead of the top slots is a different, unvalidated strategy from the one the backtests and live bots run, so the feed never returns more than the cap.

Query parameters (buy feed)

ParameterTypeDescription
symbolstring, optionalComma-separated ticker list (e.g. AAPL,MSFT). When set, only signals for those symbols are returned. Case-insensitive.
budgetnumber, optionalPer-position budget in USD. When set, each signal is annotated with a suggested share quantity and names too expensive for one share are removed. See Position sizing.
limitinteger, optionalCaps the number of entries returned, applied last (after symbol, the default slot cap, and budgetfiltering). Truncates the feed's existing rank order — deepest dip below the 7-day high for double7-guarded, highest rankScore for swing-elite — so a low limitis a genuine "top signals" subset.

symbol, budget and limit can be combined in one request — filters apply in that order, so limit caps whatever survives the symbol and budget filtering:

curl
curl "https://picckles.com/api/v1/signals/double7-guarded?symbol=AAPL,MSFT,KMI&budget=250&limit=1" \
  -H "Authorization: Bearer pk_harvest_xxxxxxxxxxxxxxxxxxxx"

Position sizing with budget

Pass ?budget=200 to have the API compute a suggested share count per signal (suggestedQuantity = floor(budget × 0.8 / price) — the first lot, with 20% of the budget kept for the possible ADD) plus the dollar amount that implies (suggestedNotional = suggestedQuantity × price), and drop any name you can’t afford a single share of. This is a convenience calculation on the public price — the signals themselves are unchanged.

Minimum. budget is per position, not a total, and each strategy publishes its own floor as minBudget so you can read it rather than hardcode it: both feeds publish $750. They put the first order at $600, which is where broker commission stops being material — it takes about a quarter of a typical trade's edge there, and roughly half at $300 an order. Below the minimum the signals are still returned; the sizing is simply not worth acting on.

budget is applied to everyentry identically — it's a per-position amount, not a total to split across the list. If you have a total allocation to deploy across the feed, divide it by slotCap yourself before calling: e.g. a $1,000 total against double7-guarded's slotCap of 6 is ?budget=166 (≈ $1,000 / 6), not ?budget=1000 — the latter sizes each of the 6 entries at $1,000 (up to $6,000 total exposure), not $1,000 total.

curl
curl "https://picckles.com/api/v1/signals/double7-guarded?budget=200" \
  -H "Authorization: Bearer pk_harvest_xxxxxxxxxxxxxxxxxxxx"
each entry gains
{
  "symbol": "KMI",
  "price": 31.45,
  "suggestedQuantity": 5,
  "suggestedNotional": 157.25,
  "reason": "Close is the lowest of the last 7 trading days, close > SMA200 ($28.90), 252-day return beats SPY, VIX < 30"
}

suggestedQuantity is the first lot on both strategies — 80% of the budget — because the remainder is reserved for a possible ADD (see addRule).

swing-elite: same entry rule, different position management

swing-elite runs the identical entry rule and entry filters as double7-guarded: the same names qualify on the same days, for the same reasons, and they exit on the same two triggers. Two position-management details differ — swing-elite adds its second lot on a 3.5% close below the first fill (double7-guarded: 3%) and has no 20-day break-even rule. The rest of what differs is two decisions about the book, and because entries is capped to slotCap, they change which positions you end up holding:

  1. Ranking. Every entry carries rankScore — how far the highest of the last 11 closes sits above this close, as a fraction (0.06 = 6%). entries is sorted by it, highest first, where double7-guarded sorts on the 7-day pctBelow7dHigh. Store it at entry: the recycling rule compares it against a laterday's candidates, so recomputing it then gives the wrong answer.
  2. Slot recycling. Once a position has been held at least 20 trading days and is still below its basis, POST /exits reports recycleEligible: true for it (pass entryDate). You may then sell it and buy any name in today's entries whose rankScore is strictly higher, both at the same next open. The live bot does this about 8 times a year. The entries feed publishes the thresholds as recycleRule so you can read them rather than hardcode them.

Implement both or neither. Under realistic trading friction the ranking change alone and the recycling alone each perform worse than plain double7-guarded; only the pair beats it. Half of this strategy is not a milder version of it.

The rs-pullback feed was retired on 1 October 2026 along with the consumer product behind it, and removed from the API. Requests for it now return 400.

The signal object

A strategy feed looks like this:

json
{
  "plan": "harvest",
  "strategy": "double7-guarded",
  "strategyName": "Swing Trade (Double 7 Guarded v2)",
  "asOf": "2026-08-12T20:05:11.000Z",
  "basedOnClose": "2026-08-12",
  "realtime": true,
  "marketOpen": false,
  "vix": 15.17,
  "vixGate": 30,
  "vixGateActive": false,
  "entries": [
    {
      "action": "BUY",
      "symbol": "KMI",
      "price": 31.45,
      "pctBelow7dHigh": -0.0421,
      "sma200": 28.90,
      "reason": "Close is the lowest of the last 7 trading days, close > SMA200 ($28.90), 252-day return beats SPY, VIX < 30, no earnings report in the prior 5 sessions"
    }
  ],
  "exitRule": {
    "description": "Buy 80% of the position budget; exit at +2% above entry, or when price makes a new 11-day high — whichever comes first (no price stop). If it closes 3% below the first fill while above its SMA200, add the reserved 20% and exit at +0.5% above the average cost instead. Once held 20 trading days from the fill, the target drops to break-even on the basis.",
    "profitTargetPct": 0.02,
    "exitLookbackDays": 11,
    "maxHoldTradingDays": 90,
    "doubledProfitTargetPct": 0.005,
    "breakEvenAfterTradingDays": 20
  },
  "entryFilterRule": {
    "description": "Already applied to entries: a qualifier is dropped if the company reported earnings (SEC 8-K Item 2.02) in the 5 sessions before the signal, or if this rule, replayed on the ticker's own history, lost on 2+ of its last 3 closed trades or lost more than 15% of the position budget on the most recent one.",
    "postEarningsSessions": 5,
    "trackLookbackTrades": 3,
    "trackMinWins": 2,
    "trackMaxLastLossPct": 0.15
  },
  "addRule": {
    "description": "Two-lot scale-in: buy 80% of the position budget on the entry signal. If a later close is 3%+ below that fill while still above the 200-day SMA, buy the remaining 20% (POST /exits returns action "ADD" for that position). The position then exits at +0.5% above the blended cost.",
    "firstLotFraction": 0.8,
    "addLotFraction": 0.2,
    "triggerPct": 0.03,
    "requiresAboveSma200": true,
    "maxLots": 2
  },
  "universeSize": 510,
  "slotCap": 6,
  "totalQualifiers": 11,
  "disclaimer": "Informational signals only — not personalized investment advice."
}
FieldMeaning
entriesToday's BUY signals — the exact list the live product would act on, ranked best-first and capped to slotCap (double7-guarded ranks by the deepest dip below the 7-day high; swing-elite by rankScore, the drop below the 11-day high). A stock not in this list either has no signal today or missed the cap (see totalQualifiers). Empty array = buy nothing today (common and valid).
actionAlways "BUY" on the entries feed (the whole array is the buy list). The /exits endpoint returns "SELL" or "HOLD".
basedOnCloseThe trade date the decision was made on.
realtimetrue = same-day/live basis (paid tiers); false = last closed bar (delayed, free tier).
vixGateActivetrue = the crash gate is on and no new entries are issued today.
exitRuleThe plain-text exit rule. To get actual SELL decisions for YOUR positions, call the /exits endpoint.
priceThe close the signal was decided on, i.e. your entry reference price.
sma200The 200-day simple moving average on the decision date — the trend filter both strategies require price to be above.
pctBelow7dHighdouble7-guarded only. How far today's close sits below the highest close of the last 7 trading days, as a fraction (e.g. -0.0261 = 2.61% below). Always ≤ 0, since qualifying requires today's close to BE that 7-day low. Used to rank entries when more qualify than slotCap.
slotCapThe concurrent-position slot count of the live product this feed mirrors — the default size of entries.
totalQualifiersHow many names passed the entry rule today, before the slot cap. Can exceed slotCap/entries.length — those extra names are intentionally not part of the feed (buying them isn’t the validated strategy).

Exit signals (SELL)

The buy feed is stateless — it doesn’t know what you hold. To get SELL decisions, POST the positions your bot holds; the API returns SELL or HOLD per position using the strategy’s exit rule. (It’s read-only; POST is used only because the request carries a body — your positions.)

entryPrice is required for both strategies — each exit rule is measured off your entry: both feeds exit at a +2% target (+0.5% above average cost once doubled) or a new 11-day high, whichever comes first. A position sent without it comes back HOLD with a reason saying so. Max 100 positions per call.

lots (1 or 2, default 1) — both feeds are a two-lot scale-in: the entries feed is the first lot (80% of your position budget). While you hold one lot, this endpoint may answer ADD — the stock closed 3%+ (Swing Trade) or 3.5%+ (Swing Elite) below your first fill while still above its 200-day SMA — meaning buy the remaining 20% (addFraction). After that, send lots: 2 with your blended average cost as entryPrice; the SELL then comes at +0.5% above it. There is never a third lot.

entryDate (optional, YYYY-MM-DD) — pass the date you entered and we'll check for a stock split between then and now, adjusting entryPrice before evaluating the exit. Without it, a split that happened after you entered will make the profit target look unreached (or already passed) against post-split closes. If the price your bot already tracks is broker-adjusted for splits, you can omit this and pass entryPrice as-is. On double7-guarded it also enables the break-even rule: once a position has been held 20 trading days from the fill, the target drops to break-even on your basis — without entryDatethat rule can't be applied.

double7-guarded — entryPrice required, entryDate optional

curl
curl -X POST "https://picckles.com/api/v1/signals/double7-guarded/exits" \
  -H "Authorization: Bearer pk_harvest_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "positions": [
      { "symbol": "AAPL", "entryPrice": 296.40, "entryDate": "2026-09-08" },
      { "symbol": "ALL",  "entryPrice": 251.20 }
    ]
  }'

swing-elite — entryDate also drives the recycling check

curl
curl -X POST "https://picckles.com/api/v1/signals/swing-elite/exits" \
  -H "Authorization: Bearer pk_harvest_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "positions": [
      { "symbol": "AAPL", "entryPrice": 302.26, "entryDate": "2026-08-05" },
      { "symbol": "ALL",  "entryPrice": 247.10, "lots": 2, "firstEntryPrice": 250.00 }
    ]
  }'

concurrentPositions (deprecated, still accepted)

Until 2026-09-29 a value of 1 selected a different exit, on the reasoning that one concentrated position has nothing to diversify against: it held a doubled position rather than selling it under water, and added a 10% catastrophe stop from the first fill. That variant has been removed.It was adopted on a measurement that filled slots in date order; re-measured with the slot ranking the live product actually uses — which at one position decides the only name you hold, so it dominates — the standard exit wins on return (26.6% vs 16.5% CAGR), on drawdown (41.5% vs 47.3%) and on average loss (−3.42% vs −5.44%), giving up only win rate (82.3% vs 85.5%).

The field is still accepted at request level and per position, so existing integrations keep working unchanged — it simply no longer affects the decision. Every position now exits on the same rule whatever your book size, and you can drop the field.

Response — one single-lot position told to ADD, one doubled position holding

json
{
  "plan": "harvest",
  "strategy": "swing-elite",
  "realtime": true,
  "exits": [
    {
      "action": "ADD",
      "symbol": "AAPL",
      "currentPrice": 295.80,
      "lots": 1,
      "reason": "ADD — closed 3%+ below your first fill ($291.60 trigger) while still above the 200-day SMA. Buy the remaining 20% of the position budget; then report lots:2 with your average cost as entryPrice.",
      "addFraction": 0.2,
      "addTriggerPrice": 291.60,
      "profitTargetPct": 0.005,
      "recycleEligible": false,
      "recycleNote": "Not recyclable: held 4 of the 20 trading days required."
    },
    {
      "action": "HOLD",
      "symbol": "ALL",
      "currentPrice": 248.90,
      "lots": 2,
      "reason": "HOLD — neither the +0.5% target above your average cost nor an 11-day high reached (max hold ~90 trading days from the first fill).",
      "profitTargetPct": 0.005,
      "recycleEligible": true,
      "recycleNote": "Recyclable: held 23 trading days and the last close ($248.90) is still below your basis ($251.40). If today's entries contain a name whose rankScore beats the rankScore you stored for this position at entry, sell this and buy that one at the same next open."
    }
  ]
}

Response (double7-guarded example above)

json
{
  "plan": "harvest",
  "strategy": "double7-guarded",
  "realtime": true,
  "exits": [
    {
      "action": "HOLD",
      "symbol": "AAPL",
      "currentPrice": 301.44,
      "reason": "HOLD — neither the +2% target nor an 11-day high reached, second lot not triggered (max hold ~90 trading days).",
      "lots": 1,
      "addTriggerPrice": 292.40,
      "profitTargetPct": 0.02,
      "exitLevel": 305.90
    },
    {
      "action": "SELL",
      "symbol": "ALL",
      "currentPrice": 262.10,
      "reason": "SELL — price reached the +2% profit target above your entry.",
      "profitTargetPct": 0.02,
      "exitLevel": 264.75
    }
  ]
}

double7-guarded results also carry profitTargetPct and exitLevel (the 11-day-high price, known from prior closes before the session opens) so your bot can rest both sells as limit orders instead of polling. For either strategy, when entryDate is set and a split is detected, the result adds splitAdjustedEntryPrice and splitRatio so you can update your own bookkeeping too.

The strategies

Swing Trade (feed id double7-guarded)

Buy a market-beating uptrend on a short-term low, with a crash gate and a two-lot scale-in.

Entry — all of:

  • Close above its 200-day average
  • Today’s close is the lowest of the last 7 trading days
  • Stock’s 1-year return beats SPY’s
  • VIX is below 30
  • No earnings report in the 5 sessions before the signal
  • This rule, replayed on the stock’s own history, won at least 2 of its last 3 trades and didn’t lose more than 15% on the last one

Exit

+2% profit target (+0.5% above average cost once you hold two lots), or price makes a new 11-day high — whichever comes first (no stop-loss). Once held 20 trading days, the target drops to break-even. Buy 80% of the position budget at the entry; add the reserved 20% if it closes 3% below that fill while still above its 200-day average.

Typical hold

~6 trading days

Swing Elite (Harvest & Orchard)

The same entry criteria and targets as above, with two differences in how the position is managed: the reserved 20% is added on a 3.5% close below the first fill (not 3%), and there is no break-even rule. The rest of what differs is which qualifiers get the slots, and when a slot is taken back.

When more names qualify than there are slots, entries are ranked by rankScore— how far the highest of the last 11 closes sits above today's close — highest first, where double7-guarded ranks on the 7-day equivalent. And once a position has been held 20 trading days and is still below its basis, the exits endpoint reports recycleEligible: true: you may sell it and buy any name in today's entries with a strictly higher rankScore, both at the same next open. Mirror both or neither — see the warning above.

Same endpoints — just swap the strategy in the path:

curl
curl https://picckles.com/api/v1/signals/swing-elite \
  -H "Authorization: Bearer pk_harvest_xxxxxxxxxxxxxxxxxxxx"
json
{
  "plan": "harvest",
  "strategy": "swing-elite",
  "strategyName": "Swing Elite",
  "asOf": "2026-08-12T20:05:11.000Z",
  "basedOnClose": "2026-08-12",
  "realtime": true,
  "marketOpen": false,
  "vix": 14.8,
  "vixGate": 30,
  "vixGateActive": false,
  "entries": [
    {
      "action": "BUY",
      "symbol": "CVS",
      "price": 93.5,
      "pctBelow7dHigh": -0.0214,
      "rankScore": 0.0613,
      "sma200": 85.17,
      "reason": "Close is the lowest of the last 7 trading days, close > SMA200 ($85.17), 252-day return beats SPY, VIX < 30, no earnings report in the prior 5 sessions"
    }
  ],
  "exitRule": {
    "description": "Buy 80% of the position budget; exit at +2% above entry, or when price makes a new 11-day high — whichever comes first (no price stop). If it closes 3.5% below the first fill while above its SMA200, add the reserved 20% and exit at +0.5% above the average cost instead.",
    "profitTargetPct": 0.02,
    "doubledProfitTargetPct": 0.005,
    "exitLookbackDays": 11,
    "maxHoldTradingDays": 90
  },
  "recycleRule": {
    "description": "Swing Elite adds one PORTFOLIO rule to the entry and exit above. Store each position's rankScore at entry. Once a position has been held at least 20 trading days and is still below its basis, it may give up its slot to any name in today's entries whose rankScore is strictly higher.",
    "minHoldTradingDays": 20,
    "requiresBelowBasis": true,
    "rankLookbackCloses": 11
  },
  "addRule": {
    "firstLotFraction": 0.8,
    "addLotFraction": 0.2,
    "triggerPct": 0.035,
    "requiresAboveSma200": true,
    "maxLots": 2
  },
  "universeSize": 510,
  "slotCap": 6,
  "totalQualifiers": 4,
  "disclaimer": "Informational signals only — not personalized investment advice."
}

And its exits: POST /api/v1/signals/swing-elite/exits — the same shape as double7-guarded (3.5% add trigger, no break-even rule), plus recycleEligible on each position when you pass entryDate. ?budget= sizes entries at the 80% first lot, same as double7-guarded.

Rate limits

Each key has a rolling 24-hour request budget set by its plan. Every response includes:

X-RateLimit-Limit: 25000
X-RateLimit-Remaining: 24997

Exceeding the limit returns 429 with a Retry-After header (seconds). Signals only change once per session (or every ~15 min intraday on real-time tiers), so you don’t need to poll aggressively — once after the close is plenty for most bots.

Errors

Standard HTTP status codes. Error responses carry an error string.

StatusMeaning
200Success.
400Bad request (e.g. an invalid budget).
401Missing or invalid API key.
403Your plan does not include that strategy.
404Unknown strategy.
429Rate limit exceeded — see Retry-After.
502Upstream market-data hiccup — retry shortly.

Plans & access

PlanStrategiesFeedRequests / day
Seed (free)Double 7 GuardedEnd-of-day50
SproutDouble 7 GuardedReal-time5,000
HarvestBothReal-time25,000
OrchardBoth + redistributionReal-time250,000

Redistributing signals inside your own product requires an Orchard license.

Build a bot

Want to try it without writing this yourself?

open-trade-bot is a free, open-source starter (Next.js) wired up to this API — a dashboard showing open positions and trade history, a daily cron for automatic runs, and a paper-trading loop you can point at a real broker whenever you're ready. Clone it, drop in a free Seed key, and it's running in a few minutes.

A minimal loop: fetch entries sized to a $200 per-position budget (see Position sizing above — up to 7 entries at $200 each here, not $200 total) and place them at your broker.

JavaScript (Node)
const KEY = process.env.PICCKLES_API_KEY;

const res = await fetch(
  "https://picckles.com/api/v1/signals/double7-guarded?budget=200",
  { headers: { Authorization: `Bearer ${KEY}` } }
);
const feed = await res.json();

if (feed.vixGateActive) {
  console.log("Crash gate on — no new entries today.");
} else {
  for (const s of feed.entries) {
    console.log(`BUY ${s.suggestedQuantity} ${s.symbol} @ ~$${s.price}`);
    // placeOrder(s.symbol, s.suggestedQuantity)  // your broker here
  }
  console.log("Exit rule:", feed.exitRule.description);
}
Python
import os, requests

key = os.environ["PICCKLES_API_KEY"]
r = requests.get(
    "https://picckles.com/api/v1/signals/double7-guarded",
    params={"budget": 200},
    headers={"Authorization": f"Bearer {key}"},
)
feed = r.json()

if feed["vixGateActive"]:
    print("Crash gate on — no new entries today.")
else:
    for s in feed["entries"]:
        print(f"BUY {s['suggestedQuantity']} {s['symbol']} @ ~$" + str(s["price"]))
    print("Exit rule:", feed["exitRule"]["description"])
Signals are informational only and identical for every subscriber — not personalized investment advice. You are responsible for your own trades.