v1

Signals API

Pull our Swing Trade and RS-Pullback 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 or rs-pullback. 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 — deepest-dip-first, up to that product's own concurrent-position slot count (6 for Swing Trade / double7-guarded, 12 for RS-Pullback). 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-first for both strategies — 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 / price); for rs-pullback it is floor(budget × 0.6 / price) — the first lot, with 40% 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: rs-pullback $1,000 and double7-guarded $750. Both 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 for double7-guarded, 60% for rs-pullback — because the remainder is reserved for a possible ADD (see addRule).

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"
    }
  ],
  "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.",
    "profitTargetPct": 0.02,
    "exitLookbackDays": 11,
    "maxHoldTradingDays": 90,
    "doubledProfitTargetPct": 0.005
  },
  "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 deepest-dip-first; rs-pullback ranks by `score`). 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.
rsi2rs-pullback only. The 2-day RSI on the decision date (must be below 10 to qualify). Lower = deeper oversold dip. Diagnostic only — it is NOT the ranking key (see score).
scorers-pullback only. Slot-selection score: the stock's 252-day return minus SPY's, less 6x ATR(14)/price. Entries are ranked by this, highest first — it is how the live product picks which qualifiers get the capital when more qualify than slotCap. Ranking by deepest RSI(2) instead measured WORSE than arbitrary order over 25 years.
maxNewPerDayrs-pullback only. The live product opens at most this many NEW positions in one session, however many qualify, so the book cannot fill up inside a single selloff day. Apply the same cap if you are mirroring it.
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: rs-pullback exits at a +2% target (+1% above average cost once you hold two lots), double7-guarded 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 (rs-pullback only, 1 or 2, default 1) — RS-Pullback is a two-lot scale-in: the entries feed is the first lot (60% of your position budget). While you hold one lot, this endpoint may answer ADD — the stock closed 2%+ below your first fill while still above its 200-day SMA — meaning buy the remaining 40% (addFraction). After that, send lots: 2 with your blended average cost as entryPrice; the SELL then comes at +1% 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.

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 }
    ]
  }'

rs-pullback — entryPrice required, entryDate and lots optional

curl
curl -X POST "https://picckles.com/api/v1/signals/rs-pullback/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 }
    ]
  }'

rs-pullback — concurrentPositions (optional)

Tell us how many positions you run at once. Only 1 changes anything: at a single position the live product uses a different exit, because one concentrated position has nothing to diversify against. It holds a doubled position rather than selling it under water, and adds a 10% catastrophe stop from the first fill — measured over 25 years at a 85.6% win rate and 34% max drawdown, against 82.3% and 58% on the standard exit. From two positions up the standard exit is better and this does nothing. Omit it and you get the standard exit, exactly as before.

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

A doubled position sitting below its first fill comes back as HOLD with the reason naming the suppressed 10-day-SMA leg, so you can tell it apart from a stale feed.

Reselling to clients on different budgets

concurrentPositions is also accepted per position, so one call can carry clients on different budgets — you do not need a separate request per cohort. Decisions come back in request order, so the same symbol may appear more than once, one row per client. The position-level value wins; the request-level one is the default for rows that omit it. We cannot know an end client's budget, so whoever holds that relationship supplies it.

json
{
  "positions": [
    { "symbol": "AAPL", "entryPrice": 302.26, "lots": 2, "firstEntryPrice": 310.00, "concurrentPositions": 1 },
    { "symbol": "AAPL", "entryPrice": 302.26, "lots": 2, "firstEntryPrice": 310.00, "concurrentPositions": 6 }
  ]
}

Same symbol, same basis, two clients — the first gets the single-position exit, the second the standard one.

rs-pullback response — one single-lot position told to ADD, one doubled position holding

json
{
  "plan": "harvest",
  "strategy": "rs-pullback",
  "realtime": true,
  "exits": [
    {
      "action": "ADD",
      "symbol": "AAPL",
      "currentPrice": 295.80,
      "lots": 1,
      "reason": "ADD — closed 2%+ below your first fill ($296.21 trigger) while still above the 200-day SMA. Buy the remaining 40% of the position budget; then report lots:2 with your average cost as entryPrice.",
      "addFraction": 0.3,
      "addTriggerPrice": 296.21,
      "profitTargetPct": 0.01
    },
    {
      "action": "HOLD",
      "symbol": "ALL",
      "currentPrice": 248.90,
      "lots": 2,
      "reason": "HOLD — +1% above average cost not reached and SMA10 not reclaimed (max hold ~15 trading days from the first fill).",
      "profitTargetPct": 0.01
    }
  ]
}

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

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). 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

RS-Pullback (Harvest & Orchard)

Buy a relative-strength leader on a fast capitulation dip.

Entry — all of:

  • Close above its 200-day average
  • Stock’s 1-year return beats SPY’s
  • RSI(2) below 10
  • Closing in the bottom 35% of the day’s range (IBS below 0.35)
  • Two consecutive down days
  • VIX is below 30

Exit

First close +2% above entry (+1% above average cost once you hold two lots), or a reclaim of the 10-day average (no stop-loss). Buy 60% of the position budget at the entry; add the reserved 40% if it closes 2% below that fill while still above its 200-day average.

Typical hold

Quick, ~2–5 days

When more names qualify than there are slots, entries are ranked by score(1-year return versus SPY, less 6× ATR/price) and at most 4 new positions are opened per session — mirror both if you are tracking the live product.

Same endpoints — just swap the strategy in the path:

curl
curl https://picckles.com/api/v1/signals/rs-pullback \
  -H "Authorization: Bearer pk_harvest_xxxxxxxxxxxxxxxxxxxx"
json
{
  "plan": "harvest",
  "strategy": "rs-pullback",
  "strategyName": "RS-Pullback",
  "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,
      "rsi2": 0.56,
      "sma200": 85.17,
      "reason": "RSI(2) 0.6 capitulation dip, beats SPY over 252d, close > SMA200, VIX < 30",
      "score": 0.1842
    }
  ],
  "exitRule": {
    "description": "Exit at the first close +2% above entry (single lot) or +1% above average cost (after the second lot), or when close reclaims the 10-day SMA (no price stop).",
    "profitTargetPct": 0.02,
    "doubledProfitTargetPct": 0.01,
    "maxHoldTradingDays": 15
  },
  "addRule": {
    "description": "Two-lot scale-in: buy 60% of the position budget on the entry signal. If a later close is 2%+ below that fill while still above the 200-day SMA, buy the remaining 40% (POST /exits returns action "ADD" for that position). The position then exits at +1% above the blended cost.",
    "firstLotFraction": 0.6,
    "addLotFraction": 0.4,
    "triggerPct": 0.02,
    "requiresAboveSma200": true,
    "maxLots": 2
  },
  "universeSize": 510,
  "slotCap": 12,
  "maxNewPerDay": 4,
  "totalQualifiers": 4,
  "disclaimer": "Informational signals only — not personalized investment advice."
}

And its exits: POST /api/v1/signals/rs-pullback/exits — same shape as double7-guarded plus the optional lots field; entryPrice required per position (the +2% target, or +1% above average cost with lots: 2), and the response can be ADD as well as SELL/HOLD. ?budget= sizes RS-Pullback entries at the 60% first lot.

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.