SSD Nodes Learn Hosting plans →
Mwongozo Matt ConnorNa Matt Connor

Jinsi ya kujenga wakala wa utafiti wa hisa kwenye VPS

Jifunze kuunda wakala wa utafiti wa hisa unaojiendesha kwenye VPS kwa kutumia DuckDB na Python. Mwongozo huu unaelezea usanidi wa systemd timer na LLM kwa uchambuzi wa soko.

Wakala wa utafiti wa hisa unaojiendesha (self-hosted)

Wakala wa utafiti wa hisa unaojiendesha ni programu ndogo kwenye seva unayomiliki inayovuta data ya soko kwa ratiba, kuihifadhi kwenye database ya ndani, kuichuja, na kuiomba large language model (LLM) kuandika mabadiliko yaliyotokea. Programu hii inasoma na kuchuja. Haifanyi biashara, na hakuna chochote katika mwongozo huu kinachopaswa kuchukuliwa kama ushauri wa kifedha.

Watu wawili wanaojenga mfumo huu kuanzia mwanzo watachagua maktaba tofauti lakini bado wataishia na sehemu nne zilezile: feed inayotoa bei na misingi ya kifedha, hifadhi ya ndani inayotunza kila safu uliyowahi kuivuta, kazi (job) inayohuisha hifadhi hiyo kwa kipima muda, na safu ya LLM inayobadilisha safu zilizosalia kuwa sentensi. Mwongozo huu unajenga muundo huo kwa kutumia Python, DuckDB, systemd timer na Claude API (application programming interface). Huyu ni wakala mdogo mwenye mzunguko maalum, kwa hivyo ikiwa ungependa kufahamu toleo la jumla kwanza, njia iliyopangwa ya kujifunza AI agents hupitia mzunguko, zana na kumbukumbu kwa mpangilio kabla ya yote hayo kugeuka kuwa tatizo la kikoa. Utekelezaji wa biashara ni kazi tofauti yenye njia zake za kufeli, na inapaswa kuwekwa kwenye VPS iliyosanidiwa kwa ajili ya trading bots badala yake.

Sehemu nne, na kazi ya kila moja

Feed ndiyo sehemu pekee inayowasiliana na ulimwengu wa nje. Inajua jinsi ya kuomba ticker na masafa ya tarehe, na jinsi ya kurejesha safu za data. Kila kitu kinachofuata husoma database yako badala ya feed, kwa hivyo hitilafu ya feed inakugharimu siku moja ya data mpya badala ya skrini iliyoharibika.

Store ndiyo lengo kuu la zoezi hili lote. Bei ya kufunga ya kila siku ambayo hukurekodi mara nyingi inaweza kuchukuliwa tena baadaye. Nukuu ya ndani ya siku, makadirio kabla hayajarekebishwa, au takwimu za msingi kabla hazijabadilishwa haziwezi kupatikana tena. Store ndiyo njia unayotumia kujenga rekodi ya kile data ilichosema kweli katika siku iliyosemwa.

Scheduler huamua wakati wa kusasisha data. Kwenye seva, hii ni systemd timer, ambayo ndiyo sababu VPS ni muhimu zaidi hapa kuliko msimbo (code) wenyewe.

LLM layer husoma kizuizi kifupi cha maandishi kilichozalishwa na SQL yako, na kuandika muhtasari wake. Haiunganishwi kamwe na database na haijengi swali (query) la SQL. Ikiwa modeli itaandika SQL, token moja mbaya inakuwa namba isiyo sahihi ndani ya sentensi fasaha, bila kitu cha kuilinganisha nayo. Ikiwa SQL inazalisha namba, modeli inaweza kukosea tu kwenye lugha, na unaweza kukagua lugha hiyo dhidi ya safu ulizotuma.

Kwa nini uendeshe kwenye VPS badala ya kompyuta ya mkononi

Kipanga ratiba (scheduler) ndiyo sababu. Soko la hisa la Marekani likifungwa saa 16:00 kwa saa za New York, ni saa 22:00 jijini Berlin na saa 04:00 asubuhi iliyofuata jijini Jakarta. Kompyuta ya mkononi huwa imelala katika nyakati zote mbili. Kazi inayokosa kutekelezwa ina gharama kubwa kuliko noti iliyochelewa: data za kila siku (daily bars) mara nyingi zinaweza kurejeshwa baadaye, lakini chochote kinachofanyiwa marekebisho hakiwezi kurejeshwa, hivyo pengo katika rekodi zako huwa la kudumu. Hoja hiyo hiyo inatumika kwa wakala yeyote ambaye ratiba yake na hali iliyohifadhiwa (stored state) lazima viendelee kuwepo baada ya reboot, jambo ambalo ndilo msingi wa kuendesha KiroCrew kama wakala anayewaka muda wote kwenye VPS yako.

Sababu ya pili ni ndogo lakini bado ni ya kweli. Seva inashikilia API key moja, kwenye faili moja, inayomilikiwa na mtumiaji mmoja wa mfumo (system user) asiye na login shell, inayotumiwa na kazi moja. Hilo ni gumu zaidi kulipanga kwenye kompyuta ya mkononi unayotumia pia kuvinjari mtandao. Weka key nje ya code na nje ya input ya model, jambo ambalo ndilo mada ya kuepuka kuweka API keys ndani ya wakala wa AI.

Kukadiria ukubwa: diski, RAM na tokeni

Diski ni sehemu rahisi. Mstari mmoja wa kila siku ni safu moja kwa kila ticker kwa kila siku ya biashara, na mwaka wa biashara wa Marekani una takriban siku 252.

ChatiRows in the prices table by watchlist size, at 252 trading days a year
Data iliyo nyuma ya chati hii
[
  {
    "label": "20 tickers",
    "rows_after_1y": "5,040",
    "rows_after_10y": "50,400"
  },
  {
    "label": "100 tickers",
    "rows_after_1y": "25,200",
    "rows_after_10y": "252,000"
  },
  {
    "label": "500 tickers",
    "rows_after_1y": "126,000",
    "rows_after_10y": "1,260,000"
  }
]

Ticker ishirini ni 5,040 safu baada ya mwaka mmoja. Mstari wa 500 tickers unafikia 1,260,000 safu baada ya miaka kumi. Kila safu ina tarehe na namba chache za aina ya double, na DuckDB huhifadhi safu wima zikiwa zimefinyazwa (compressed), kwa hivyo huu ni ukubwa wa makumi ya megabytes badala ya gigabytes. Usiamini makadirio hayo, ikiwemo yangu. Endesha du -h /opt/research/data/market.duckdb baada ya backfill yako ya kwanza na utumie namba yako mwenyewe.

RAM ndipo VPS ndogo inapopata shida. DuckDB kwa chaguo-msingi huchukua sehemu kubwa ya kumbukumbu ya mashine na cores zote kwa ajili ya query moja, jambo ambalo ni sahihi kwenye seva ya uchanganuzi lakini si sahihi kwenye mashine ya 2 GB inayofanya kazi nyingine. Aggregation moja juu ya jedwali zima la bei husababisha mchakato huo kusitishwa na kernel, na systemd huripoti Main process exited, code=killed, status=9/KILL wakati journalctl -k ikionyesha kuwa kumbukumbu imeisha (out of memory kill). Weka memory_limit na threads wazi ili query iwe polepole badala ya kufa.

Tokeni zinapaswa kupimwa, si kukadiriwa. Kila jibu la Messages API hubeba kitu cha usage chenye input_tokens na output_tokens. Andika vyote kwenye jedwali kila unapopiga simu, na baada ya wiki moja utajua kiasi chako halisi, ambacho unakizidisha kwa bei ya modeli yako siku unayokagua. Mambo mawili ni thabiti vya kutosha kupangia. Tokeni za pato (output tokens) zina bei ya juu kuliko tokeni za ingizo (input tokens) kwenye kila modeli ya Claude, kwa hivyo kupunguza ujumbe hadi maneno 200 kunapunguza gharama zaidi kuliko kupunguza data unayotuma. Na prompt caching haisaidii kazi inayofanyika mara moja kwa siku. Cache hudumu dakika 5 kwa chaguo-msingi, na muda mrefu zaidi wa maisha ambao Anthropic hutoa ni saa 1, ambayo inagharimu mara 2 ya bei ya msingi ya ingizo badala ya mara 1.25 kwa cache ya dakika 5. Vyovyote vile, kufikia wakati wa kazi ya siku inayofuata, block iliyokuwa kwenye cache itakuwa imekwisha muda wake na utalipa bei kamili ya ingizo tena. Caching hulipa pale kazi moja inapopiga simu nyingi juu ya block moja kubwa ya maandishi.

Sakinisha vipengele

sudo apt update
sudo apt install -y python3-venv
sudo useradd --system --create-home --home-dir /opt/research --shell /usr/sbin/nologin research
sudo -u research python3 -m venv /opt/research/venv
sudo -u research /opt/research/venv/bin/pip install duckdb pandas yfinance anthropic
sudo install -d -o research -g research -m 750 /opt/research/data

Kagua usakinishaji kabla ya kuandika msimbo wowote:

sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'

Hiyo huchapisha ok. Ikiwa uliruka mazingira ya virtual na kuendesha pip install dhidi ya Python ya mfumo, Ubuntu 24.04 itakuzuia kwa error: externally-managed-environment, kwa sababu usambazaji unamiliki /usr/lib/python3 na unakataa kuruhusu pip kuandika hapo. Venv si suala la adabu. Ni saraka pekee ambayo pip inaruhusiwa kuigusa.

Ufunguo wa API huwekwa kwenye faili ambayo mtumiaji wa huduma anaweza kuisoma na hakuna mwingine anayeweza:

sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/env

Weka mstari mmoja ndani yake, bila alama za kunukuu na bila export, kwa sababu systemd huchakata faili hii yenyewe badala ya kuipitisha kwenye shell:

ANTHROPIC_API_KEY=sk-ant-your-key-here

Hifadhi: majedwali mawili

# /opt/research/store.py
import duckdb

DB = '/opt/research/data/market.duckdb'

SCHEMA = [
    """
    CREATE TABLE IF NOT EXISTS prices (
      ticker VARCHAR,
      day    DATE,
      open   DOUBLE,
      high   DOUBLE,
      low    DOUBLE,
      close  DOUBLE,
      volume BIGINT,
      PRIMARY KEY (ticker, day)
    )
    """,
    """
    CREATE TABLE IF NOT EXISTS runs (
      started_at    TIMESTAMPTZ,
      model         VARCHAR,
      input_tokens  BIGINT,
      output_tokens BIGINT,
      hits          BIGINT
    )
    """,
]

def connect(read_only=False):
    con = duckdb.connect(DB, read_only=read_only)
    con.execute("SET memory_limit='512MB'")
    con.execute('SET threads=2')
    if not read_only:
        for statement in SCHEMA:
            con.execute(statement)
    return con

Primary key kwenye (ticker, day) ndiyo inayofanya refresh kuwa salama kurudiwa. INSERT OR REPLACE huandika juu ya safu iliyopo tayari kwa ticker hiyo na siku hiyo, kwa hivyo kuendesha backfill mara mbili hakuzidishi ukubwa wa jedwali. Bila key hiyo, kurudia operesheni baada ya crash kutazalisha nakala mbili za kila bar kimyakimya, na kila wastani utakaokokotoa baadaye utakuwa na makosa bila ujumbe wowote wa hitilafu kukuonya.

DuckDB inaruhusu mchakato mmoja pekee kushikilia faili ikiwa wazi kwa ajili ya kuandika. Mwandishi wa pili anashindwa mara moja na Could not set lock on file, ikifuatiwa na PID inayoshikilia faili hiyo, ambayo kwa kawaida ni shell ya duckdb uliyoiacha wazi kwenye terminal nyingine. Muunganisho wa kusoma pekee (read-only) haukwepi tatizo hili. Nyaraka za concurrency za DuckDB zinaelezea njia mbili: mchakato mmoja unaosoma na kuandika, au michakato kadhaa inayosoma pekee, kamwe si mchanganyiko wa yote mawili. connect inakubali flag ya read_only ili wasomaji kadhaa waweze kushiriki faili hiyo, jambo linalofanya kazi tu wakati hakuna mwandishi aliyeifungua. Ikiwa michakato kadhaa inahitaji kuandika kwa wakati mmoja, hiyo ni kazi ya engine nyingine: SQLite katika mode ya WAL inaruhusu wasomaji kufanya kazi wakati mwandishi mmoja anafanya commit, na busy timeout huwafanya waandishi wengine kusubiri badala ya kushindwa. DuckDB dhidi ya SQLite kwa mzigo wa kazi wa seva inalinganisha mifumo hiyo miwili, na kuendesha SQLite katika uzalishaji kwenye VPS inashughulikia mipangilio inayofanya WAL ifanye kazi ipasavyo.

Kazi ya kusasisha (refresh job)

# /opt/research/refresh.py
import sys
import pandas as pd
import yfinance as yf
from store import connect

TICKERS = ['AAPL', 'MSFT', 'KO', 'SAP', 'TSM']
FIRST_DAY = '2016-01-01'
COLS = ['ticker', 'day', 'open', 'high', 'low', 'close', 'volume']

def fetch(ticker, start):
    df = yf.Ticker(ticker).history(start=start, auto_adjust=True)
    if df.empty:
        return None
    df = df.reset_index()
    stamps = pd.to_datetime(df['Date'])
    if stamps.dt.tz is not None:
        stamps = stamps.dt.tz_localize(None)
    df['day'] = stamps.dt.date
    df['ticker'] = ticker
    df = df.rename(columns={'Open': 'open', 'High': 'high', 'Low': 'low',
                            'Close': 'close', 'Volume': 'volume'})
    return df[COLS]

def main():
    con = connect()
    empty = 0
    for ticker in TICKERS:
        last = con.execute('SELECT max(day) FROM prices WHERE ticker = ?',
                           [ticker]).fetchone()[0]
        rows_df = fetch(ticker, str(last) if last else FIRST_DAY)
        if rows_df is None:
            print(ticker + ': feed returned no rows', file=sys.stderr)
            empty += 1
            continue
        con.register('rows_df', rows_df)
        con.execute('INSERT OR REPLACE INTO prices '
                    'SELECT ticker, day, open, high, low, close, volume FROM rows_df')
        print(ticker + ': ' + str(len(rows_df)) + ' rows')
    con.close()
    if empty == len(TICKERS):
        print('every ticker returned nothing: the feed is broken', file=sys.stderr)
        sys.exit(1)

main()

Iendeshe kwa mikono mara moja:

sudo -u research /opt/research/venv/bin/python /opt/research/refresh.py

Uendeshaji wa kwanza hujaza data ya miaka iliyopita na kuchapisha mstari mmoja kwa kila ticker ikiwa na idadi ya maelfu ya safu. Iendeshe tena baada ya dakika moja na kila mstari utaonyesha safu 1 au 2, kwa sababu kazi hiyo huanza kutoka siku ya mwisho iliyohifadhiwa tayari. Uendeshaji huo wa pili ndio jaribio la kweli: kama idadi bado iko katika maelfu, max(day) hairejeshi chochote na kitendo cha kuingiza data (insert) kinaandika upya historia yako yote kila usiku.

Ukaguzi wa df.empty ndio mstari muhimu zaidi katika faili. Ticker.history() haitoi kosa (raise) kwa alama (symbol) isiyo sahihi au iliyoondolewa kwenye soko. Inachapisha onyo kuhusu alama hiyo ikiwezekana kuondolewa kwenye soko bila data ya bei kupatikana (maneno kamili hubadilika kati ya matoleo ya maktaba) na kurejesha DataFrame tupu. Kazi isiyo na ukaguzi huo haitaandika chochote, itatoka kwa exit code 0, na systemd itaonyesha uendeshaji wenye afya (kijani) wakati jedwali linaacha kukua kimyakimya. Utagundua wiki kadhaa baadaye, kutoka kwenye skrini inayorejesha safu zilezile kila siku.

Ushughulikiaji wa timestamp una sababu pia. Index inayorejeshwa na feed inaweza kubeba timezone ya soko la hisa, na kuondoa offset si sawa na kubadilisha kwenda UTC. Kikao cha Tokyo kilichowekwa alama ya saa sita usiku kwa saa za huko hubadilika na kuwa siku ya kalenda iliyotangulia katika UTC, kwa hivyo ubadilishaji wa UTC hubadilisha kimyakimya kila bar ya Kijapani kurudi nyuma siku moja na kuharibu primary key. tz_localize(None) huhifadhi tarehe ya kikao cha soko lenyewe, ambayo ndiyo maana ya bar ya kila siku.

Timer inayotekelezwa wakati wa kufungwa kwa soko

Soko la Marekani hufungwa saa 16:00 kwa saa za New York, na data za mwisho huchukua dakika chache kukamilika, kwa hivyo kazi hii huanza saa 16:20. Andika muda huo kwa saa za New York, si kwa UTC. New York iko UTC minus 5 wakati wa baridi na UTC minus 4 wakati wa kiangazi, kwa hivyo timer iliyoandikwa kwa saa ya UTC isiyobadilika itapishana kwa saa moja mara mbili kwa mwaka na kuanza kufanya kazi kabla ya soko kufungwa. systemd 235 na matoleo mapya zaidi hukubali timezone moja kwa moja ndani ya OnCalendar, na Ubuntu 24.04 inakuja na 255. Thibitisha toleo lako kwa systemctl --version.

# /etc/systemd/system/research-refresh.service
[Unit]
Description=Refresh market data and run the daily screen
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=research
Group=research
WorkingDirectory=/opt/research
EnvironmentFile=/etc/research/env
ExecStart=/opt/research/venv/bin/python /opt/research/refresh.py
ExecStart=/opt/research/venv/bin/python /opt/research/screen.py
# /etc/systemd/system/research-refresh.timer
[Unit]
Description=Run the refresh after the US market close

[Timer]
OnCalendar=Mon-Fri 16:20 America/New_York
Persistent=true
RandomizedDelaySec=180

[Install]
WantedBy=timers.target

Type=oneshot ndiyo aina pekee ya huduma inayokubali ExecStart zaidi ya moja, na huiendesha kwa mpangilio, ikisimama ikiwa moja itatoka na status isiyo zero. Hiyo ndiyo tabia unayohitaji hasa: refresh iliyofeli haipaswi kufuatiwa na onyesho la data ya zamani. Persistent=true ni muhimu kwenye VPS inayowaka upya (reboot) kwa ajili ya updates za kernel. Reboot saa 16:15 bila hiyo na kazi hiyo itapotea; ukiwa nayo, kazi hiyo huanza mara tu mashine inapokuwa tayari. Agent inayopaswa kubaki ikiwa imewaka badala ya kufanya kazi na kuzima inahitaji muundo tofauti wa unit, mchakato wa muda mrefu wenye sheria za Restart= na bila timer yoyote, jambo ambalo kuendesha dsh bila kichwa kwenye VPS kwa kutumia systemd linaelezea.

sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timer

list-timers inapaswa kuonyesha safu ya NEXT iliyo na muda wa kazi wa siku inayofuata ya kazi, iliyogeuzwa kuwa saa ya ndani ya seva. Orodha tupu inamaanisha kuwa timer haijawezeshwa, au unit inakosa sehemu yake ya [Install], kwa hivyo enable haikuwa na kitu cha kuunganisha kwenye timers.target.

Skrini: SQL kwanza, modeli mwisho

SQL ni sahihi na haina gharama kwa kila uendeshaji. Modeli si mojawapo ya hayo. Kwa hivyo, hoja (query) hupunguza upeo, na ni yale matokeo yaliyosalia pekee ndiyo yanayotumwa kwenye modeli. Hifadhi hii kama /opt/research/screen.sql.

WITH ma AS (
  SELECT ticker, day, close,
         avg(close) OVER w20 AS ma20,
         avg(close) OVER w50 AS ma50,
         row_number() OVER (PARTITION BY ticker ORDER BY day) AS n
  FROM prices
  WINDOW
    w20 AS (PARTITION BY ticker ORDER BY day ROWS BETWEEN 19 PRECEDING AND CURRENT ROW),
    w50 AS (PARTITION BY ticker ORDER BY day ROWS BETWEEN 49 PRECEDING AND CURRENT ROW)
)
SELECT ticker, day, close, round(ma20, 2) AS ma20, round(ma50, 2) AS ma50
FROM ma
WHERE n > 50 AND ma20 > ma50
ORDER BY day DESC, ticker
LIMIT 20;

Kichujio cha n > 50 si pambo. ROWS BETWEEN 49 PRECEDING AND CURRENT ROW hupiga wastani wa safu zozote zilizopo, kwa hivyo safu ya tatu ya ticker hurejesha wastani wa siku tatu na bado huiita ma50. Linganisha hilo na ma20 na utaunda crossover mwanzoni mwa historia ya kila ticker ambayo haikutokea kamwe. Kuchuja kulingana na nambari ya safu huondoa safu ambazo dirisha lake halikuwa limejaa.

Kile ambacho modeli hukiona, na kile ambacho haikioni kamwe

# /opt/research/screen.py
from anthropic import Anthropic
from store import connect

SYSTEM = (
    'You are a research assistant. Use only the rows in the message. '
    'If a number is not in the rows, say that it is not available. '
    'Do not give investment advice, price targets or buy and sell calls. '
    'Write at most 200 words.'
)

con = connect()
sql = open('/opt/research/screen.sql').read()
rows = con.execute(sql).fetchall()
block = '\n'.join(' / '.join(str(v) for v in row) for row in rows)

client = Anthropic(max_retries=5)   # reads ANTHROPIC_API_KEY from the environment
resp = client.messages.create(
    model='claude-sonnet-5',
    max_tokens=600,
    thinking={'type': 'disabled'},
    system=SYSTEM,
    messages=[{'role': 'user', 'content': 'Screen hits, ticker / day / close / ma20 / ma50:\n' + block}],
)

print(resp.content[0].text)
con.execute('INSERT INTO runs VALUES (now(), ?, ?, ?, ?)',
            ['claude-sonnet-5', resp.usage.input_tokens,
             resp.usage.output_tokens, len(rows)])
con.close()

Mstari wa thinking ni muhimu kwenye modeli hii. Claude Sonnet 5 hufikiri kwa chaguo-msingi, tokeni zake za kufikiri huhesabiwa kama matokeo (output) na huongezeka kwenye max_tokens, na hurudi kama kizuizi (block) kabla ya maandishi. Bila mstari huo, resp.content[0] inaweza kuwa kizuizi cha kufikiri, na utafutaji wa .text utafeli. Claude Sonnet 5 sasa ni modeli ya zamani (legacy). Mrithi wake, claude-sonnet-5-5, hukataa disabled, na mpangilio wa kutumia hapo ni thinking={'type': 'between_tools'}, ambao huzima kufikiri kabla ya jibu.

Kikomo cha maneno katika system prompt hupunguza nusu ya gharama kubwa ya bili. Maelekezo ya kutumia safu mlalo (rows) ulizopewa pekee ndiyo kitu unachopaswa kukithibitisha badala ya kukiamini: futa safu wima (column) moja kutoka kwenye kizuizi, iendeshe tena, na usome matokeo. Ikiwa namba ya safu hiyo bado inaonekana, modeli ilijaza pengo hilo, na prompt yako haina mkazo wa kutosha. Jaribio hilo huchukua dakika mbili na ndiyo njia pekee ya kweli ya kujua.

Modeli haioni kamwe API key, haioni kamwe njia ya database (database path), na haiendeshi kamwe query. Inapokea safu mlalo na kurudisha maandishi. Mpaka huo ndio unaofanya matokeo yaweze kukaguliwa, kwa sababu kila namba kwenye dokezo inapaswa pia kuonekana kwenye kizuizi ulichotuma, na unaweza kuzilinganisha mstari kwa mstari. Wakati unapopanua ingizo hilo ili kujumuisha kurasa zilizotolewa kutoka kwenye mtandao, mpaka huo hubadilika umbo, kwa sababu kichwa cha habari au chapisho la jukwaani linaweza kubeba maelekezo yaliyokusudiwa modeli badala ya ukweli uliokusudiwa wewe, ambayo ndiyo hatari inayotokana na kumpa wakala backend ya utafutaji ya SearXNG. Kwa mengi zaidi kuhusu upande wa prompting wa hili, kutumia Claude kwa uchambuzi wa kifedha huenda zaidi katika kile ambacho modeli ina uwezo mzuri wa kukisoma.

Utekelezaji mmoja, kuanzia mwanzo hadi mwisho

Saa 16:20 kwa saa za New York, kipima muda huanzisha huduma. refresh.py huomba data ya kila ticker kuanzia siku ya mwisho iliyohifadhiwa, huandika pau moja au mbili mpya kwa kila moja, na kuchapisha mstari mmoja kwa kila ticker. screen.py hufungua faili ileile, huendesha hoja ya moving average, na kupata safu chache. Safu hizo huwa kizuizi cha maandishi cha token mia chache. Wito mmoja wa API huibadilisha kuwa dokezo fupi, dokezo hilo huenda kwenye jarida, na safu moja hutua kwenye runs ikiwa na idadi ya token na idadi ya matokeo yaliyopatikana.

journalctl -u research-refresh.service -n 50 --no-pager

Logi yenye afya huwa na mstari mmoja kwa kila ticker, ikifuatiwa na dokezo, kisha research-refresh.service: Deactivated successfully. Ikiwa unasoma logi hiyo kutoka kwenye kikao cha Claude Code kwenye seva wakati kikao cha pili kinafanya hoja ya skrini, vikao hivyo viwili vinaweza kupitishiana maandishi badala ya wewe kunakili mistari kati ya terminal. Baada ya wiki moja, soma gharama yako mwenyewe kutoka kwenye database:

SELECT count(*) AS runs,
       sum(input_tokens)  AS in_tokens,
       sum(output_tokens) AS out_tokens
FROM runs;

Zidisha jumla hizo kwa bei za kila milioni ya token ambazo modeli yako inatoa siku unayozisoma. Hiyo inakupa takwimu halisi badala ya makadirio ya mtu mwingine. Bei zilizochapishwa hubadilika. Hesabu haibadiliki.

Kwa nini backtests hupata overfit, na jinsi ya kufuatilia hilo

Andika upya screen kama utendaji wa urefu wa madirisha mawili, pitia gridi ya jozi, na uzipange kulingana na faida. Jozi bora itaonekana kuwa nzuri sana. Hilo ndilo tatizo, si matokeo. Gridi ya jozi 200 ni majaribio 200, na umehifadhi ile iliyopata bahati zaidi.

Unaweza kuona hilo likitokea ndani ya dakika kumi. Gawanya hifadhi yako mara mbili kulingana na tarehe. Pitia gridi kwenye nusu ya kwanza pekee na uandike mshindi. Pitia gridi hiyo hiyo kwenye nusu ya pili. Ikiwa washindi wawili wako mbali sana, vigezo hivyo vinafit kelele (noise), na jozi inayoshinda kwenye nusu uliyoiwekea vigezo haisemi chochote kuhusu kesho.

Survivorship ni mbaya zaidi kuliko overfitting kwa sababu kurekebisha vigezo hakuwezi kuiondoa. Orodha yako ya ticker ni wanachama wa index wa leo, kwa hivyo inajumuisha tu kampuni zilizosalia. Ikiwa utaiomba feed ticker iliyoondolewa kwenye orodha mwaka 2019, itakupa data tupu, jambo linalomaanisha kuwa kampuni hiyo haijawahi kuingia kwenye hifadhi yako na haijawahi kuingia kwenye jaribio lako. Kila backtest unayoendesha imeshawatenga wale waliofeli.

Restated fundamentals huvunja mpangilio wa muda. Takwimu ya mapato ambayo API inatoa leo kwa robo ya mwaka ya 2019 si lazima iwe takwimu iliyochapishwa mwaka 2019. Screen inayochanganya fundamentals za leo na bei za 2019 inatumia taarifa ambazo hazikuwepo wakati huo. Bei kwa kawaida ni salama hapa. Fundamentals kwa kawaida si salama.

Adjusted prices hubadilika chini yako. Kwa auto_adjust=True bei za kufunga (closes) hurekebishwa nyuma kwa ajili ya gawio na migawanyo ya hisa (splits), kwa hivyo swali lilelile likiendeshwa mwezi ujao litarudisha historia tofauti kidogo. Kuhifadhi safu ulizotumia kihalisi ndiko kunakofanya matokeo yaweze kurudiwa, na ndiyo sababu nyingine ya kuwepo kwa hifadhi ya ndani (local store).

Backtest pia hupuuza kamisheni na slippage, na inadhani kuwa oda yako haibadilishi bei. Mambo hayo yanahusu utekelezaji, ambao uko nje ya wigo wa hapa na yamejadiliwa katika kuendesha trading bots kwenye VPS.

Njia za kushindwa na ujumbe utakaouona

error: externally-managed-environment wakati pip inapoendeshwa. Upo nje ya virtual environment. Iite /opt/research/venv/bin/pip kwa kutumia njia kamili (full path).

Could not set lock on file, ikifuatiwa na PID. Mchakato mwingine umefungua faili ya DuckDB kwa ajili ya kuandika, kwa kawaida ni interactive shell uliyosahau kuifunga. Ifunge. Kufungua muunganisho wa pili kwa read_only=True hakusaidii wakati mwandishi huyo bado yuko wazi, kwa sababu DuckDB inaruhusu michakato kadhaa kusoma au mchakato mmoja kuandika, si vyote kwa wakati mmoja.

Main process exited, code=killed, status=9/KILL katika systemctl status. Kernel imesitisha kazi hiyo kwa sababu ya ukosefu wa kumbukumbu (memory). Thibitisha kwa journalctl -k | grep -i oom, kisha punguza memory_limit katika store.py.

Uendeshaji uliofanikiwa (green run) lakini hauandiki chochote. systemctl status inasoma active (exited) na jedwali halijakua. Feed imerudisha fremu tupu. Kushindwa huku hufichika kwa muda mrefu zaidi, kwa hivyo fanya kazi hiyo itoke (exit) kwa status isiyo ya sifuri wakati kila ticker inaporudi ikiwa tupu.

Timer imewaka katika siku ya mapumziko ya soko (market holiday). systemd haijui kalenda ya soko la hisa, kwa hivyo Mon-Fri inajumuisha siku za mapumziko. Uendeshaji hutokea, feed haina jipya, na kazi hiyo inapaswa kulichukulia hilo kama jambo la kawaida badala ya kulihesabu kama kosa.

429 kutoka kwa API. Umezidi kikomo cha kasi (rate limit). Anthropic SDK inajaribu tena yenyewe kwa kutumia backoff, na Anthropic(max_retries=5) inaongeza idadi ya majaribio. Ikiwa bado inashindwa kila siku, kazi hiyo inaomba vitu vingi sana kwa mkupuo mmoja.

Hiki si kitu gani

Hii ni zana ya kusaidia utafiti. Mfano wa AI unaofanya muhtasari wa faili hutoa tafsiri ya faili hiyo, na unaweza kukosea kwa kujiamini kuhusu namba iliyoandikwa kwenye matini. Hii ndiyo sababu kila takwimu katika dokezo hili lazima irejelee mstari ulioutuma. Ichukulie matokeo haya kama orodha fupi ya mambo ya kusoma wewe mwenyewe. Hakuna kitu hapa ambacho ni ushauri wa kifedha, na hakuna hata kimoja kinachoweza kuchukuliwa kama ishara ya biashara.

Backtests ni muhimu kwa kukataa mawazo mabaya, lakini ni dhaifu katika kuthibitisha mawazo mazuri. Mkakati unaofeli kwenye data yako mwenyewe umekufa kweli. Mkakati unaofaulu umepona tu kwenye data yako, jambo ambalo ni madai madogo sana kuliko inavyoweza kuhisiwa saa 7 usiku.

Utekelezaji wa amri umewekwa nje ya wigo huu kwa makusudi. Amri na vitambulisho vya broker hubeba wasifu wa hatari tofauti na kisanduku cha utafiti cha kusoma pekee (read-only), na kuvichanganya huweka funguo za biashara kwenye mashine moja na LLM prompt. Ikiwa unataka kuona mahali ambapo muundo huu unakaa kando ya mambo mengine yanayofaa kuendeshwa kwenye seva yako mwenyewe, mawakala wa AI wanaojiendesha wenyewe wanaofaa kuendeshwa ndio mwongozo mpana zaidi.

FAQ

Je, nahitaji huduma ya kulipia ya data ya soko?

Si kwa ajili ya prototype. Huduma ya bure isiyo rasmi inafaa kwa kujifunza muundo wa mfumo, lakini itafeli, kwa sababu inategemea tovuti ambayo haina wajibu wowote kwako. Kwa kawaida hufeli kwa kutoa fremu tupu badala ya kutoa exception, kwa hivyo kazi yako lazima ikague idadi ya safu (row counts). Hamia kwenye huduma ya kulipia yenye API iliyoandikwa vizuri na anwani ya usaidizi pindi data inapoanza kutumika kufanya maamuzi. Hifadhi (store) ndiyo inayofanya mabadiliko hayo kuwa rahisi: ni kazi ya kuchota data (fetch function) pekee inayobadilika, huku ratiba, schema na skrini zikibaki kama zilivyo.

Je, niweke bei kwenye SQLite au DuckDB?

DuckDB ni ya safu wima (columnar) na imeundwa kwa ajili ya kuchanganua safu nyingi ili kukokotoa jumla, jambo ambalo ndilo hasa wastani wa miondoko (moving average) ya miaka kumi ya data. SQLite inaelekea kwenye safu mlalo (row oriented) na ni bora zaidi kwa usomaji na uandishi mdogo mdogo kutoka kwa michakato mingi kwa wakati mmoja. Kwa kazi moja iliyoratibiwa inayoongeza mamia ya safu na kisha kuchanganua mamilioni, DuckDB inafaa zaidi. Ikiwa michakato mingi lazima iandike kwa wakati mmoja, SQLite katika hali ya WAL inaruhusu wasomaji kufanya kazi wakati mwandishi mmoja akifanya commit, na busy timeout huwafanya waandishi wengine kusubiri badala ya kufeli moja kwa moja.

Gharama za simu za LLM kwa mwezi ni kiasi gani?

Ingiza usage.input_tokens na usage.output_tokens kutoka kwa kila jibu kwenye jedwali, kisha zidisha jumla ya kila wiki kwa bei ya kila token milioni ambayo modeli yako inaorodhesha siku unayokagua. Hiyo ndiyo namba pekee inayobaki kuwa sahihi katika robo ijayo. Uendeshaji mmoja wa kila siku kwenye skrini fupi ni idadi ndogo ya simu, na urefu wa dokezo ndiyo sehemu unayoweza kuidhibiti: kupunguza muhtasari usizidi maneno 200 huokoa zaidi kuliko kutuma safu chache, kwa sababu token za pato (output tokens) zina bei ya juu kuliko token za ingizo (input tokens) kwenye kila modeli ya Claude.

Kwa nini kazi yangu ilifanikiwa lakini haikuandika safu mpya?

Kuna sababu mbili za kawaida. Soko lilikuwa limefungwa, kwa sababu ratiba ya systemd Mon-Fri inajumuisha likizo za soko la hisa. Au huduma ya data ilirejesha fremu tupu kwa kila ticker, jambo ambalo maktaba za mteja mara nyingi huripoti kama onyo lililochapishwa badala ya exception, kwa hivyo mchakato bado unamalizika kwa exit 0 na systemd bado inaonyesha uendeshaji wa kijani. Zitofautishe kwa kulinganisha SELECT max(day) FROM prices dhidi ya siku ya mwisho ya kweli ya biashara, na ufanye kazi hiyo imalizike kwa exit isiyo sifuri (non zero) wakati kila ticker inaporejea tupu.

Je, wakala (agent) anaweza kuamua nini cha kununua?

Hapana, na kujaribu kumjenga ili afanye hivyo ndiko miradi hii inakosea. Modeli haina ufikiaji wa soko, haina mtazamo wa nafasi yako au hali yako ya kodi, na haina njia ya kukagua namba zake dhidi ya kitu chochote. Inachoweza kufanya vizuri ni kusoma kiasi kikubwa cha maandishi na kukuambia ni vitu vichache vipi vinavyostahili umakini wako leo. Hakuna chochote inachoandika ambacho ni ushauri wa kifedha, na uamuzi, pamoja na wajibu wake, unabaki kwako.