Compare commits

...

12 Commits

Author SHA1 Message Date
1f83b6abf9 fix: type error 2023-10-27 00:01:27 +09:00
10324d5799 feat: add api 2023-10-26 23:58:33 +09:00
ad7ab6db86 chore: version update 2023-10-26 22:12:16 +09:00
ca9dd95461 feat: add pages 2023-10-26 22:11:58 +09:00
fae9cc8154 feat: better index css 2023-10-26 22:11:26 +09:00
e6d7020fc8 fix: signal_listener 2023-10-26 22:10:47 +09:00
32c1458a9c feat: show page description 2023-10-26 22:09:44 +09:00
22fad337ae fix: cache error 2023-10-26 22:09:20 +09:00
454850c6b3 refactor: StockList 2023-07-28 17:52:07 +09:00
ea611f0cdc add 120 2023-07-28 17:27:21 +09:00
d31855785d chore: remove unused. 2023-07-25 18:18:06 +09:00
f089a04ef4 feat: animation disable when diff > 200 2023-07-25 10:55:42 +09:00
28 changed files with 668 additions and 362 deletions

3
.gitignore vendored
View File

@ -10,3 +10,6 @@ stock.db
.env.test.local .env.test.local
.env.production.local .env.production.local
.env.local .env.local
# Fresh build directory
_fresh/

View File

@ -1,3 +1,5 @@
# Stock # Stock
[![Made with Fresh](https://fresh.deno.dev/fresh-badge.svg)](https://fresh.deno.dev)
주식 데이터 수집 및 선별하는 파이썬 코드입니다. 주식 데이터 수집 및 선별하는 파이썬 코드입니다.

24
app.py
View File

@ -1,24 +0,0 @@
import flask
import argparse
parser = argparse.ArgumentParser(description="Stock web server")
parser.add_argument("--port", type=int, default=12001, help="port number")
parser.add_argument("--host", type=str, default="0.0.0.0", help="host address")
parser.add_argument("--debug", action="store_true", help="debug mode")
app = flask.Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
@app.route("/dist/<m>")
def distServe(m:str):
return flask.send_from_directory("dist", m)
@app.route("/")
def index():
import pages
return flask.render_template("index.html", pages = pages.GenLists)
if __name__ == '__main__':
args = parser.parse_args()
app.run(host=args.host, port=args.port, debug=args.debug)

11
db/db.ts Normal file
View File

@ -0,0 +1,11 @@
import { Kysely, ParseJSONResultsPlugin } from "kysely";
import { DB as Sqlite } from "sqlite";
import { DenoSqliteDialect } from "./deno-sqlite-dialect.ts";
import { Database } from "./type.ts";
export const db = new Kysely<Database>({
dialect: new DenoSqliteDialect({
database: new Sqlite("stock.db")
}),
plugins: [new ParseJSONResultsPlugin()]
});

View File

@ -0,0 +1,27 @@
/// The MIT License (MIT)
/// Copyright (c) 2023 Alex Gleason
/// Copyright (c) 2022 Sami Koskimäki
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
import type { SqliteDialectConfig } from 'kysely';
/** Type compatible with both [dyedgreen/deno-sqlite](https://github.com/dyedgreen/deno-sqlite) and [denodrivers/sqlite3](https://github.com/denodrivers/sqlite3). */
type DenoSqlite =
& {
close(): void;
changes: number;
lastInsertRowId: number;
}
& ({
queryEntries(sql: string, params: any): unknown[];
} | {
prepare(sql: string): {
all(...params: any): unknown[];
};
});
interface DenoSqliteDialectConfig extends Omit<SqliteDialectConfig, 'database'> {
database: DenoSqlite | (() => Promise<DenoSqlite>);
}
export type { DenoSqlite, DenoSqliteDialectConfig };

49
db/deno-sqlite-dialect.ts Normal file
View File

@ -0,0 +1,49 @@
/// The MIT License (MIT)
/// Copyright (c) 2023 Alex Gleason
/// Copyright (c) 2022 Sami Koskimäki
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
import {
type DatabaseIntrospector,
type Dialect,
type DialectAdapter,
type Driver,
Kysely,
type QueryCompiler,
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler,
} from 'kysely';
import { DenoSqliteDriver } from './kysely-sqlite-driver.ts';
import type { DenoSqliteDialectConfig } from './deno-sqlite-dialect-config.ts';
class DenoSqliteDialect implements Dialect {
readonly #config: DenoSqliteDialectConfig;
constructor(config: DenoSqliteDialectConfig) {
this.#config = Object.freeze({ ...config });
}
createDriver(): Driver {
return new DenoSqliteDriver(this.#config);
}
createQueryCompiler(): QueryCompiler {
return new SqliteQueryCompiler();
}
createAdapter(): DialectAdapter {
return new SqliteAdapter();
}
// deno-lint-ignore no-explicit-any
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new SqliteIntrospector(db);
}
}
export { DenoSqliteDialect };

112
db/kysely-sqlite-driver.ts Normal file
View File

@ -0,0 +1,112 @@
/// The MIT License (MIT)
/// Copyright (c) 2023 Alex Gleason
/// Copyright (c) 2022 Sami Koskimäki
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
import { CompiledQuery, type DatabaseConnection, type Driver, type QueryResult } from 'kysely';
import type { DenoSqlite, DenoSqliteDialectConfig } from './deno-sqlite-dialect-config.ts';
class DenoSqliteDriver implements Driver {
readonly #config: DenoSqliteDialectConfig;
readonly #connectionMutex = new ConnectionMutex();
#db?: DenoSqlite;
#connection?: DatabaseConnection;
constructor(config: DenoSqliteDialectConfig) {
this.#config = Object.freeze({ ...config });
}
async init(): Promise<void> {
this.#db = typeof this.#config.database === 'function' ? await this.#config.database() : this.#config.database;
this.#connection = new DenoSqliteConnection(this.#db);
if (this.#config.onCreateConnection) {
await this.#config.onCreateConnection(this.#connection);
}
}
async acquireConnection(): Promise<DatabaseConnection> {
// SQLite only has one single connection. We use a mutex here to wait
// until the single connection has been released.
await this.#connectionMutex.lock();
return this.#connection!;
}
async beginTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('begin'));
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('commit'));
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await connection.executeQuery(CompiledQuery.raw('rollback'));
}
// deno-lint-ignore require-await
async releaseConnection(): Promise<void> {
this.#connectionMutex.unlock();
}
// deno-lint-ignore require-await
async destroy(): Promise<void> {
this.#db?.close();
}
}
class DenoSqliteConnection implements DatabaseConnection {
readonly #db: DenoSqlite;
constructor(db: DenoSqlite) {
this.#db = db;
}
executeQuery<O>({ sql, parameters }: CompiledQuery): Promise<QueryResult<O>> {
const rows = 'queryEntries' in this.#db
? this.#db.queryEntries(sql, parameters)
: this.#db.prepare(sql).all(...parameters);
const { changes, lastInsertRowId } = this.#db;
return Promise.resolve({
rows: rows as O[],
numAffectedRows: BigInt(changes),
insertId: BigInt(lastInsertRowId),
});
}
// deno-lint-ignore require-yield
async *streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error('Sqlite driver doesn\'t support streaming');
}
}
class ConnectionMutex {
#promise?: Promise<void>;
#resolve?: () => void;
async lock(): Promise<void> {
while (this.#promise) {
await this.#promise;
}
this.#promise = new Promise((resolve) => {
this.#resolve = resolve;
});
}
unlock(): void {
const resolve = this.#resolve;
this.#promise = undefined;
this.#resolve = undefined;
resolve?.();
}
}
export { DenoSqliteDriver };

64
db/type.ts Normal file
View File

@ -0,0 +1,64 @@
import { ColumnType, Generated, Insertable, Selectable, Updateable } from "kysely";
/**
* "Code" TEXT,
"Date" TEXT,
"Close" INTEGER NOT NULL,
"Diff" INTEGER NOT NULL,
"Open" INTEGER NOT NULL,
"High" INTEGER NOT NULL,
"Low" INTEGER NOT NULL,
"Volume" INTEGER NOT NULL,
*/
export interface StockTable {
Code: string;
Date: string;
Close: number;
Diff: number;
Open: number;
High: number;
Low: number;
Volume: number;
}
export interface KRXCorpTable{
Name: string;
/**
* PK
*/
Code: string;
Sector: string;
Product: string;
ListingDay: string;
ClosingMonth: string;
Representative: string;
Homepage: string;
AddressArea: string;
LastUpdate: string;
}
export interface KOSPITable{
Name: string;
/**
* PK
*/
Code: string;
}
export interface KOSDAQTable{
Name: string;
/**
* PK
*/
Code: string;
}
export interface Database {
stock: StockTable;
KRXCorp: KRXCorpTable;
KOSPI: KOSPITable;
KOSDAQ: KOSDAQTable;
}

View File

@ -2,31 +2,28 @@
"lock": false, "lock": false,
"tasks": { "tasks": {
"start": "deno run -A --watch=static/,routes/ dev.ts", "start": "deno run -A --watch=static/,routes/ dev.ts",
"update": "deno run -A -r https://fresh.deno.dev/update ." "prod_start": "deno run -A main.ts",
}, "update": "deno run -A -r https://fresh.deno.dev/update .",
"lint": { "build": "deno run -A dev.ts build",
"rules": { "preview": "deno run -A main.ts"
"tags": [
"fresh",
"recommended"
]
}
}, },
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
"imports": { "imports": {
"$fresh/": "https://deno.land/x/fresh@1.3.1/", "$fresh/": "https://deno.land/x/fresh@1.5.2/",
"preact": "https://esm.sh/preact@10.15.1", "preact": "https://esm.sh/preact@10.18.1",
"preact/": "https://esm.sh/preact@10.15.1/", "preact/": "https://esm.sh/preact@10.18.1/",
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0", "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2",
"@preact/signals": "https://esm.sh/*@preact/signals@1.1.3", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0",
"auto-animate": "https://esm.sh/@formkit/auto-animate@0.7.0", "auto-animate": "https://esm.sh/@formkit/auto-animate@0.7.0",
"auto-animate/": "https://esm.sh/@formkit/auto-animate@0.7.0/", "auto-animate/": "https://esm.sh/@formkit/auto-animate@0.7.0/",
"twind": "https://esm.sh/twind@0.16.19", "twind": "https://esm.sh/twind@0.16.19",
"twind/": "https://esm.sh/twind@0.16.19/", "twind/": "https://esm.sh/twind@0.16.19/",
"$std/": "https://deno.land/std@0.193.0/" "$std/": "https://deno.land/std@0.203.0/",
"kysely": "npm:kysely@^0.26.3",
"kysely/helpers/sqlite": "npm:kysely@^0.26.3/helpers/sqlite",
"sqlite": "https://deno.land/x/sqlite@v3.8/mod.ts"
}, },
"compilerOptions": { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
"jsx": "react-jsx", "exclude": ["**/_fresh/*"]
"jsxImportSource": "preact"
}
} }

View File

@ -1,36 +1,42 @@
// DO NOT EDIT. This file is generated by fresh. // DO NOT EDIT. This file is generated by Fresh.
// This file SHOULD be checked into source version control. // This file SHOULD be checked into source version control.
// This file is automatically updated during development when running `dev.ts`. // This file is automatically updated during development when running `dev.ts`.
import * as $0 from "./routes/_404.tsx"; import * as $0 from "./routes/_404.tsx";
import * as $1 from "./routes/_app.tsx"; import * as $1 from "./routes/_app.tsx";
import * as $2 from "./routes/api/joke.ts"; import * as $2 from "./routes/api/corps/[index].ts";
import * as $3 from "./routes/api/kosdaq.ts"; import * as $3 from "./routes/api/corps/index.ts";
import * as $4 from "./routes/api/kospi.ts"; import * as $4 from "./routes/api/joke.ts";
import * as $5 from "./routes/api/pages/[name].ts"; import * as $5 from "./routes/api/kosdaq.ts";
import * as $6 from "./routes/api/pages/index.ts"; import * as $6 from "./routes/api/kospi.ts";
import * as $7 from "./routes/greet/[name].tsx"; import * as $7 from "./routes/api/pages/[name].ts";
import * as $8 from "./routes/index.tsx"; import * as $8 from "./routes/api/pages/index.ts";
import * as $9 from "./routes/pages/[name].tsx"; import * as $9 from "./routes/greet/[name].tsx";
import * as $10 from "./routes/index.tsx";
import * as $11 from "./routes/pages/[name].tsx";
import * as $$0 from "./islands/Counter.tsx"; import * as $$0 from "./islands/Counter.tsx";
import * as $$1 from "./islands/StockList.tsx"; import * as $$1 from "./islands/Search.tsx";
import * as $$2 from "./islands/StockList.tsx";
const manifest = { const manifest = {
routes: { routes: {
"./routes/_404.tsx": $0, "./routes/_404.tsx": $0,
"./routes/_app.tsx": $1, "./routes/_app.tsx": $1,
"./routes/api/joke.ts": $2, "./routes/api/corps/[index].ts": $2,
"./routes/api/kosdaq.ts": $3, "./routes/api/corps/index.ts": $3,
"./routes/api/kospi.ts": $4, "./routes/api/joke.ts": $4,
"./routes/api/pages/[name].ts": $5, "./routes/api/kosdaq.ts": $5,
"./routes/api/pages/index.ts": $6, "./routes/api/kospi.ts": $6,
"./routes/greet/[name].tsx": $7, "./routes/api/pages/[name].ts": $7,
"./routes/index.tsx": $8, "./routes/api/pages/index.ts": $8,
"./routes/pages/[name].tsx": $9, "./routes/greet/[name].tsx": $9,
"./routes/index.tsx": $10,
"./routes/pages/[name].tsx": $11,
}, },
islands: { islands: {
"./islands/Counter.tsx": $$0, "./islands/Counter.tsx": $$0,
"./islands/StockList.tsx": $$1, "./islands/Search.tsx": $$1,
"./islands/StockList.tsx": $$2,
}, },
baseUrl: import.meta.url, baseUrl: import.meta.url,
}; };

53
gen.py
View File

@ -5,7 +5,6 @@ import sqlite3
from typing import Dict, List from typing import Dict, List
from render import * from render import *
import db as database import db as database
from jinja2 import Environment, PackageLoader, select_autoescape
import pandas as pd import pandas as pd
import tqdm import tqdm
@ -92,7 +91,7 @@ def isMACDCrossSignal(signal: pd.Series, macd: pd.Series, nday: int, order=1) ->
signal.iloc[nday+order] < macd.iloc[nday+order]) signal.iloc[nday+order] < macd.iloc[nday+order])
def isRelativeDiffLessThan(a:pd.Series,b:pd.Series, threshold: float,nday:int) -> bool: def isRelativeDiffLessThan(a:pd.Series,b:pd.Series, threshold: float,nday:int) -> bool:
return abs(a.iloc[nday] - b.iloc[nday]) / b.iloc[nday] < threshold return abs(a.iloc[nday] - b.iloc[nday + 1]) / b.iloc[nday + 1] < threshold
def isDiffGreaterThan(a:pd.Series,b:pd.Series, nday:int) -> bool: def isDiffGreaterThan(a:pd.Series,b:pd.Series, nday:int) -> bool:
"""a is bigger than b""" """a is bigger than b"""
@ -145,7 +144,6 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
d5 = d(5) d5 = d(5)
d20 = d(20) d20 = d(20)
d25 = d(25) d25 = d(25)
d30 = d(30)
d45 = d(45) d45 = d(45)
d60 = d(60) d60 = d(60)
d120 = d(120) d120 = d(120)
@ -156,8 +154,14 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
bollinger_upperband = d25 + 2* d_std25 bollinger_upperband = d25 + 2* d_std25
a = [d5, d20, d45, d60] a = [d5, d20, d45]
for nday in ndays: for nday in ndays:
if openv[nday] <= d240[nday] and d240[nday] <= close[nday] and d240[nday + 1] < d240[nday]:
collector.collect("양봉사이240일선증가", corp, stock.index[nday])
if d5[nday + 1] < d5[nday] and d5[nday + 2] > d5[nday + 1] and d20[nday + 1] < d20[nday]:
collector.collect("5일선반등120선증가", corp, stock.index[nday])
if openv[nday] <= d20[nday] and d20[nday] <= close[nday]: if openv[nday] <= d20[nday] and d20[nday] <= close[nday]:
collector.collect("양봉사이20일선", corp, stock.index[nday]) collector.collect("양봉사이20일선", corp, stock.index[nday])
@ -207,6 +211,9 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
if (D240BiggerThanYesterDay): if (D240BiggerThanYesterDay):
collector.collect("240일 증가", corp, stock.index[nday]) collector.collect("240일 증가", corp, stock.index[nday])
if max([d[nday] for d in (d20, d60, d120)]) < min(close[nday], openv[nday]):
collector.collect("떠있음", corp, stock.index[nday])
if (d60[nday + 1] < d60[nday]): if (d60[nday + 1] < d60[nday]):
collector.collect("정배열60", corp, stock.index[nday]) collector.collect("정배열60", corp, stock.index[nday])
if (d20[nday + 1] < d20[nday]): if (d20[nday + 1] < d20[nday]):
@ -217,6 +224,11 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
d120[nday + 1] <= d120[nday]): d120[nday + 1] <= d120[nday]):
collector.collect("모두 정배열", corp, stock.index[nday]) collector.collect("모두 정배열", corp, stock.index[nday])
if(d120[nday + 1] <= d120[nday] and
d120[nday + 1] < d240[nday] and
d120[nday] >= d240[nday]):
collector.collect("120선240선추월", corp, stock.index[nday])
if (d5[nday + 1] < d20[nday + 1] and d20[nday] < d5[nday]): if (d5[nday + 1] < d20[nday + 1] and d20[nday] < d5[nday]):
collector.collect("d20d5돌파", corp, stock.index[nday]) collector.collect("d20d5돌파", corp, stock.index[nday])
@ -248,8 +260,6 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
#rsi_signal = macd.loc[::-1].ewm(span=7).mean().loc[::-1] #rsi_signal = macd.loc[::-1].ewm(span=7).mean().loc[::-1]
parser = argparse.ArgumentParser(description="주식 검색 정보를 출력합니다.") parser = argparse.ArgumentParser(description="주식 검색 정보를 출력합니다.")
parser.add_argument("--format", "-f", choices=["json", "html"], default="html",
help="출력 포맷을 지정합니다. 기본값은 html입니다.")
parser.add_argument("--dir", "-d", default=".", help="출력할 폴더를 지정합니다.") parser.add_argument("--dir", "-d", default=".", help="출력할 폴더를 지정합니다.")
parser.add_argument("--corp", "-c", help="주식 코드를 지정합니다. 지정하지 않으면 kosdaq과 kospi만 검색합니다.") parser.add_argument("--corp", "-c", help="주식 코드를 지정합니다. 지정하지 않으면 kosdaq과 kospi만 검색합니다.")
parser.add_argument("--fullSearch", help="모든 주식을 검색합니다.", action='store_true') parser.add_argument("--fullSearch", help="모든 주식을 검색합니다.", action='store_true')
@ -268,10 +278,6 @@ if __name__ == "__main__":
if args.corp: if args.corp:
krx_corps = [corp for corp in krx_corps if corp.Code == args.corp] krx_corps = [corp for corp in krx_corps if corp.Code == args.corp]
env = Environment(
loader=PackageLoader('render', 'templates'),
autoescape=select_autoescape(['html', 'xml'])
)
collector = OutputCollector() collector = OutputCollector()
prepareCollector(collector) prepareCollector(collector)
@ -281,25 +287,10 @@ if __name__ == "__main__":
dataStore.clearCache() dataStore.clearCache()
for k,v in collector.data.items(): for k,v in collector.data.items():
if args.format == "json": data = json.dumps(v.toDict(), ensure_ascii=False)
data = json.dumps(v.toDict(), indent=4, ensure_ascii=False) if args.printStdout:
if args.printStdout: print(k)
print(k) print(data)
print(data)
else:
with open(os.path.join(args.dir, k + ".json"), "w", encoding="UTF-8") as f:
f.write(data)
else: else:
template = env.get_template("Lists.html") with open(os.path.join(args.dir, k + ".json"), "w", encoding="UTF-8") as f:
f.write(data)
days = v.corpListByDate.keys()
days = list(days)
days.sort(reverse=True)
days = days[:5]
html = template.render(collected=v, title=k, days=days, lastUpdate=datetime.date.today().isoformat())
if args.printStdout:
print(html)
else:
with open(os.path.join(args.dir, k + ".html"), "w", encoding="UTF-8") as f:
f.write(html)

5
islands/Search.tsx Normal file
View File

@ -0,0 +1,5 @@
export default function Search(){
return <div>
<div>div</div>
</div>
}

View File

@ -1,9 +1,18 @@
import { Button } from "../components/Button.tsx"; import { Button } from "../components/Button.tsx";
import { useEffect, useRef } from "preact/hooks"; import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
import { ComponentChildren } from "preact"; import { ComponentChildren } from "preact";
import { Signal, useSignal } from "@preact/signals"; import { Signal, useSignal } from "@preact/signals";
import { IS_BROWSER } from "$fresh/runtime.ts"; import { IS_BROWSER } from "$fresh/runtime.ts";
import { mapValues } from "$std/collections/map_values.ts"; import { mapValues } from "$std/collections/map_values.ts";
import { useAsync } from "../util/util.ts";
import {
Coperation,
CorpSimple,
fetchKosdaqList,
fetchKospiList,
fetchPageInfo,
PageCorpsInfo,
} from "../util/api.ts";
interface StockProps { interface StockProps {
pageName: string; pageName: string;
@ -31,94 +40,70 @@ function ToggleButton(props: ToggleButtonProps) {
); );
} }
type QueryStatus<T> = { function StockListByDate(
type: "loading"; { prevSet, rows, name }: {
} | { prevSet: Set<string>;
type: "complete"; rows: Coperation[];
data: T; name: string;
} | { },
type: "error"; ) {
err: Error; const lastCount = useRef(rows.length);
}; const curCount = rows.length;
const parent = useRef<HTMLDivElement>(null);
function useAsync<T>(fn: () => Promise<T>): Signal<QueryStatus<T>> { const controller = useRef<
const state = useSignal({ {
type: "loading", isEnabled: () => boolean;
} as QueryStatus<T>); disable: () => void;
enable: () => void;
} | undefined
>();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { console.log("animation mount on ", name);
const data = await fn(); const { default: autoAnimate } = await import(
state.value = { "https://esm.sh/@formkit/auto-animate@0.7.0"
type: "complete", );
data: data, if (parent.current) {
}; const cntr = autoAnimate(parent.current);
} catch (err) { controller.current = cntr;
state.value = {
type: "error",
err: err,
};
} }
})(); })();
}, []); }, [parent]);
return state;
}
interface Coperation { useLayoutEffect(() => {
Name: string; if (controller.current) {
Code: string; if (Math.abs(curCount - lastCount.current) > 200) {
Sector: string; console.log("disable animation", curCount, "from", lastCount.current);
Product: string; controller.current.disable();
ListingDay: string; } else {
ClosingMonth: string; console.log("enable animation", curCount, "from", lastCount.current);
Representative: string; controller.current.enable();
Homepage: string; }
AddressArea: string; lastCount.current = curCount;
LastUpdate: string; }
} }, [parent, rows]);
interface PageCorpsInfo { return (
name: string; <div ref={parent}>
description: string; <h2 class="text-lg">{name}</h2>
corpListByDate: Record<string, Coperation[]>; {rows.map((row) => {
} const firstOccur = !prevSet.has(row.Code);
return (
interface CorpSimple { <div
code: string; key={row.Code}
name: string; class={[
} "bg-white",
firstOccur ? "text-[#ff5454] underline" : "text-black",
function StockListByDate({prevSet, rows, name}:{prevSet:Set<string>, ].join(" ")}
rows:Coperation[], >
name: string}){ <a href={`https://stockplus.com/m/stocks/KOREA-A${row.Code}`}>
const parent = useRef<HTMLDivElement>(null); {row.Name}
useEffect(()=>{ </a>
(async ()=>{ </div>
console.log("animation mount on ",name); );
const {default:autoAnimate} = await import("https://esm.sh/@formkit/auto-animate@0.7.0"); })}
parent.current && autoAnimate(parent.current) </div>
})(); );
},[parent]);
return <div ref={parent}>
<h2 class="text-lg">{name}</h2>
{rows.map((row) => {
const firstOccur = !prevSet.has(row.Code);
return (
<div
key={row.Code}
class={[
"bg-white",
firstOccur ? "text-[#ff5454]" : "text-black",
].join(" ")}
>
<a href={`https://stockplus.com/m/stocks/KOREA-A${row.Code}`}>
{row.Name}
</a>
</div>
);
})}
</div>
} }
function StockList({ data }: { data: PageCorpsInfo }) { function StockList({ data }: { data: PageCorpsInfo }) {
@ -135,14 +120,13 @@ function StockList({ data }: { data: PageCorpsInfo }) {
const prevSet = i == 0 ? new Set<string>() : sets[i - 1]; const prevSet = i == 0 ? new Set<string>() : sets[i - 1];
const rows = corpListByDate[x]; const rows = corpListByDate[x];
return ( return (
<StockListByDate key={x} name={x} prevSet={prevSet} rows={rows}></StockListByDate> <StockListByDate key={x} name={x} prevSet={prevSet} rows={rows} />
); );
})} })}
</div> </div>
); );
} }
type FilterInfoOption = { type FilterInfoOption = {
list: { list: {
items: CorpSimple[]; items: CorpSimple[];
@ -155,7 +139,7 @@ function filterInfo(info: Coperation[], filterList: FilterInfoOption) {
const checkMap = new Map<string, boolean>(); const checkMap = new Map<string, boolean>();
for (const l of filterList.list) { for (const l of filterList.list) {
for (const i of l.items) { for (const i of l.items) {
checkMap.set(i.code, l.include); checkMap.set(i.Code, l.include);
} }
} }
return info.filter((x) => { return info.filter((x) => {
@ -169,17 +153,13 @@ function filterInfo(info: Coperation[], filterList: FilterInfoOption) {
} }
export default function StockListUI(props: StockProps) { export default function StockListUI(props: StockProps) {
const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(async () => { const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(() =>
const res = await Promise.all([ Promise.all([
fetch("/api/pages/" + encodeURIComponent(props.pageName)), fetchPageInfo(props.pageName),
fetch("/api/kospi"), fetchKospiList(),
fetch("/api/kosdaq"), fetchKosdaqList(),
]); ])
const corpsInfo = await res[0].json() as PageCorpsInfo; );
const kospi = await res[1].json();
const kosdaq = await res[2].json();
return [corpsInfo, kospi, kosdaq];
});
const viewKospi = useSignal(true); const viewKospi = useSignal(true);
const viewKosdaq = useSignal(false); const viewKosdaq = useSignal(false);
const viewOtherwise = useSignal(false); const viewOtherwise = useSignal(false);
@ -203,34 +183,47 @@ export default function StockListUI(props: StockProps) {
<p>File Loading Failed</p> <p>File Loading Failed</p>
</div> </div>
) )
: <StockList data={applyFilter(sig.value.data[0], sig.value.data[1], sig.value.data[2])}></StockList>} : (
<StockList
data={applyFilter(
sig.value.data[0],
sig.value.data[1],
sig.value.data[2],
)}
/>
)}
</div> </div>
)} )}
</div> </div>
</div> </div>
); );
function applyFilter(data: PageCorpsInfo, kospi: CorpSimple[], kosdaq: CorpSimple[]): PageCorpsInfo{ function applyFilter(
const filter = getFilters(kospi,kosdaq); data: PageCorpsInfo,
kospi: CorpSimple[],
kosdaq: CorpSimple[],
): PageCorpsInfo {
const filter = getFilters(kospi, kosdaq);
return { return {
name: data.name, name: data.name,
description: data.description, description: data.description,
corpListByDate: mapValues(data.corpListByDate, (it: Coperation[])=>{ corpListByDate: mapValues(data.corpListByDate, (it: Coperation[]) => {
return filterInfo(it, filter); return filterInfo(it, filter);
}) }),
} };
} }
function getFilters(kospi: CorpSimple[], kosdaq: CorpSimple[]): FilterInfoOption{ function getFilters(
kospi: CorpSimple[],
kosdaq: CorpSimple[],
): FilterInfoOption {
return { return {
otherwise: viewOtherwise.value, otherwise: viewOtherwise.value,
list: [{ list: [{
include: viewKospi.value, include: viewKospi.value,
items: kospi items: kospi,
}, }, {
{ include: viewKosdaq.value,
include: viewKosdaq.value, items: kosdaq,
items: kosdaq }],
} };
]
}
} }
} }

View File

@ -9,9 +9,15 @@ import "$std/dotenv/load.ts";
import { start } from "$fresh/server.ts"; import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts"; import manifest from "./fresh.gen.ts";
import twindPlugin from "$fresh/plugins/twind.ts"; import twindPlugin from "$fresh/plugins/twindv1.ts";
import twindConfig from "./twind.config.ts"; import twindConfig from "./twind.config.ts";
console.log("start");
Deno.addSignalListener("SIGINT", () => {
Deno.exit(0);
});
await start(manifest, { await start(manifest, {
port: 12001, port: 12001,
plugins: [twindPlugin(twindConfig)] plugins: [twindPlugin(twindConfig)]

View File

@ -26,18 +26,25 @@ function watchFile(
} }
} }
})(); })();
Deno.addSignalListener("SIGINT", () => { const closeHandler = () => {
watcherRef.close(); watcherRef.close();
}); };
Deno.addSignalListener("SIGINT", closeHandler);
return ()=>{
Deno.removeSignalListener("SIGINT", closeHandler);
closeHandler();
}
} }
let pages_meta: PageDescription[] = []; let pages_meta: PageDescription[] = [];
let mtime = 0; let mtime = 0;
let lastest_disposer = () => {};
export async function get_pages_meta(): Promise<[PageDescription[],number]>{ export async function get_pages_meta(): Promise<[PageDescription[],number]>{
if (pages_meta) { if (pages_meta.length == 0) {
pages_meta = await readPagesDescription(); pages_meta = await readPagesDescription();
mtime = Date.now(); mtime = Date.now();
watchFile(PAGES_PATH, async () => { lastest_disposer();
lastest_disposer = watchFile(PAGES_PATH, async () => {
pages_meta = await readPagesDescription(); pages_meta = await readPagesDescription();
mtime = Date.now(); mtime = Date.now();
}); });

View File

@ -36,11 +36,11 @@
macd가 아래로 내려가는 시점을 찾습니다. macd 는 5일선과 10일선으로 이루어지고 macd가 아래로 내려가는 시점을 찾습니다. macd 는 5일선과 10일선으로 이루어지고
시그널을 구하기 위한 이동 평균은 4일입니다. 시그널을 구하기 위한 이동 평균은 4일입니다.
- name: 뭉침 - name: 뭉침
description: d5, d20, d45, d60 만난것 종가 5% 이내 description: d5, d20, d45 만난것 종가 5% 이내
- name: 뭉침01 - name: 뭉침01
description: d5, d20, d45, d60 만난것 종가 1% 이내 description: d5, d20, d45 만난것 종가 1% 이내
- name: 뭉침03 - name: 뭉침03
description: d5, d20, d45, d60 만난것 종가 3% 이내 description: d5, d20, d45 만난것 종가 3% 이내
- name: 45일선 반등 - name: 45일선 반등
description: 45일 선반등 description: 45일 선반등
@ -64,3 +64,13 @@
description: '볼린저 밴드(25일선 ,표준편차 2배)의 위 밴드 값을 넘었을 때 표시.' description: '볼린저 밴드(25일선 ,표준편차 2배)의 위 밴드 값을 넘었을 때 표시.'
- name: 양봉사이20일선 - name: 양봉사이20일선
description: Open과 Close 사이 20일 선 description: Open과 Close 사이 20일 선
- name: 양봉사이240일선증가
description: Open과 Close 사이 240일 선. 240일 선 증가
- name: 떠있음
description: |
양봉, 음봉이 20일선, 60일선, 120선보다 떠있으면
- name: 5일선반등120선증가
description: 5일선이 반등 120 선 증가
- name: 120선240선추월
description: |
120선이 상승해서 240일 선을 뚫을때

View File

@ -0,0 +1,35 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../db/db.ts";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
export const handler: Handlers = {
async GET(req, ctx): Promise<Response> {
const headers = new Headers({
"content-type": "application/json"
});
const index = ctx.params.index;
const corp = await db.selectFrom("KRXCorp")
.selectAll([
"KRXCorp"
])
.select(eb=> [
jsonArrayFrom(eb.selectFrom("stock")
.select([
"stock.Close",
"stock.Open",
"stock.Low",
"stock.High",
"stock.Date",
"stock.Volume",
])
.where("Code", "=", index)
.orderBy("Date", "desc")
.limit(100)
).as("prices")]
)
.where("Code", "=", index)
.executeTakeFirst();
return new Response(JSON.stringify(corp ?? null), {headers});
},
}

21
routes/api/corps/index.ts Normal file
View File

@ -0,0 +1,21 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../db/db.ts";
export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> {
const headers = new Headers({
"content-type": "application/json"
});
const url = new URL(req.url);
const q = url.searchParams.get("q");
const name = url.searchParams.get("name");
const corps = await db.selectFrom("KRXCorp")
.selectAll([
"KRXCorp"
])
.$if(!!q, qb=> qb.where("Name", "like", "%"+q+"%"))
.$if(!!name, qb => qb.where("Name", "=", name))
.execute();
return new Response(JSON.stringify(corps), {headers});
},
}

View File

@ -1,17 +1,17 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import {DB} from "https://deno.land/x/sqlite/mod.ts"; import { db } from "../../db/db.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> { async GET(req, _ctx): Promise<Response> {
const headers = new Headers({ const headers = new Headers({
"content-type": "application/json" "content-type": "application/json"
}); });
const db = new DB("stock.db"); const rows = await db.selectFrom("KOSDAQ")
const conn = db.query("SELECT Code,Name FROM KOSDAQ"); .select([
const body = conn.map(row=>({ "Code",
code: row[0], "Name"
name: row[1] ])
})) .execute();
return new Response(JSON.stringify(body), {headers}); return new Response(JSON.stringify(rows), {headers});
}, },
} }

View File

@ -1,17 +1,17 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import {DB} from "https://deno.land/x/sqlite/mod.ts"; import { db } from "../../db/db.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> { async GET(req, _ctx): Promise<Response> {
const headers = new Headers({ const headers = new Headers({
"content-type": "application/json" "content-type": "application/json"
}); });
const db = new DB("stock.db"); const rows = await db.selectFrom("KOSPI")
const conn = db.query("SELECT Code,Name FROM KOSPI"); .select([
const body = conn.map(row=>({ "Code",
code: row[0], "Name"
name: row[1] ])
})) .execute();
return new Response(JSON.stringify(body), {headers}); return new Response(JSON.stringify(rows), {headers});
}, },
} }

View File

@ -25,16 +25,18 @@ export const handler: Handlers = {
const mtime = stat.mtime ?? new Date(0); const mtime = stat.mtime ?? new Date(0);
const body = await Deno.readTextFile(path); const body = await Deno.readTextFile(path);
headers.set("last-modified", mtime.toUTCString()); headers.set("last-modified", mtime.toUTCString());
console.log(mtime);
// headers.set("cache-control", "max-age=600");
const ifModifiedSinceValue = req.headers.get("if-modified-since"); // const ifModifiedSinceValue = req.headers.get("if-modified-since");
if ( ifModifiedSinceValue && // if ( ifModifiedSinceValue &&
mtime.getTime() != new Date(ifModifiedSinceValue).getTime() // mtime.getTime() <= new Date(ifModifiedSinceValue).getTime()
){ // ){
return new Response(null, { // return new Response(null, {
status: Status.NotModified, // status: Status.NotModified,
statusText: STATUS_TEXT[Status.NotModified] // statusText: STATUS_TEXT[Status.NotModified]
}) // })
} // }
return new Response(body, {headers}); return new Response(body, {headers});
}, },
}; };

View File

@ -1,25 +1,21 @@
import { Head } from "$fresh/runtime.ts"; import { Head } from "$fresh/runtime.ts";
import { useSignal } from "@preact/signals"; import { get_pages_meta, PageDescription } from "../pages.ts";
import {Button} from "../components/Button.tsx";
import { PageDescription, get_pages_meta } from "../pages.ts";
import { Handlers, PageProps } from "$fresh/server.ts"; import { Handlers, PageProps } from "$fresh/server.ts";
export const handler: Handlers = { export const handler: Handlers = {
async GET(_req, ctx){ async GET(_req, ctx) {
const [pages,_] = await get_pages_meta(); const [pages, _] = await get_pages_meta();
return await ctx.render(pages); return await ctx.render(pages);
} },
} };
export default function Home({data}: PageProps<PageDescription[]>) {
const count = useSignal(3);
export default function Home({ data }: PageProps<PageDescription[]>) {
return ( return (
<> <>
<Head> <Head>
<title>stock-front</title> <title>stock-front</title>
</Head> </Head>
<div class="px-4 py-8 mx-auto bg-[#86efac]"> <div class="px-4 py-8 mx-auto bg-[#86efac] min-h-screen">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center"> <div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<img <img
class="my-6" class="my-6"
@ -31,14 +27,16 @@ export default function Home({data}: PageProps<PageDescription[]>) {
<h1 class="text-4xl font-bold">Stock</h1> <h1 class="text-4xl font-bold">Stock</h1>
<div class="my-4"> <div class="my-4">
<ul> <ul>
{ {data.map((x) => (
data.map(x=><li class="my-2"> <li class="my-2">
<a class="p-2 block hover:bg-gray-300 bg-white rounded" href={`/pages/${encodeURIComponent(x.name)}`}> <a
class="p-2 block hover:bg-gray-300 bg-white rounded"
href={`/pages/${encodeURIComponent(x.name)}`}
>
{x.name} {x.name}
</a> </a>
</li> </li>
) ))}
}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -1,14 +1,26 @@
import { PageProps } from "$fresh/server.ts"; import { PageProps, Handlers } from "$fresh/server.ts";
import { Head } from "$fresh/runtime.ts"; import { Head } from "$fresh/runtime.ts";
import { get_pages_meta, PageDescription } from "../../pages.ts";
import StockList from "../../islands/StockList.tsx"; import StockList from "../../islands/StockList.tsx";
export const handler: Handlers = {
async GET(_req, ctx) {
const [pages, _] = await get_pages_meta();
const name = ctx.params.name;
const page = pages.filter(x=> x.name === name);
if (page.length === 0) {
return await ctx.renderNotFound();
}
return await ctx.render(page[0]);
},
};
export default function Pages(props: PageProps) { export default function Pages(props: PageProps<PageDescription>) {
return <> return <>
<Head> <Head>
<title>Stock: {props.params.name}</title> <title>Stock: {props.params.name}</title>
</Head> </Head>
<div class="px-4 py-8 mx-auto bg-[#86efac]"> <div class="px-4 py-8 mx-auto bg-[#86efac] min-h-screen">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center"> <div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<img <img
class="my-6" class="my-6"
@ -18,6 +30,7 @@ export default function Pages(props: PageProps) {
alt="stock graph" alt="stock graph"
/> />
<h1 class="text-4xl">{props.params.name}</h1> <h1 class="text-4xl">{props.params.name}</h1>
<p>{props.data.description}</p>
<StockList pageName={props.params.name}></StockList> <StockList pageName={props.params.name}></StockList>
</div> </div>
</div> </div>

View File

@ -1,60 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock</title>
<style>
body{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(to right, #2b2b2b, #3d1844);
color: #fff;
}
.table_item:nth-child(2n){
background: #a7a7a7;
}
.table_item:nth-child(2n+1){
background: #fff;
}
.table_item:hover{
background: #8d8d8d;
}
.container{
display: grid;
grid-template-rows: 24px auto;
background: #f0f0f0;
color: black;
box-shadow: 0px 0px 5px 0px white;
text-decoration: none;
grid-template-columns: repeat({{ 5 }}, 1fr);
}
.container a:link, a:visited{
text-decoration: none;
color: black;
}
.data_header{
border-bottom: 1px solid #a7a7a7;
}
</style>
</head>
<body>
<div style="margin: auto; max-width: 750px;">
<h1>{{title}} Stock List</h1>
<h3>{{lastUpdate}}</h3>
<section class="description">
{{collected.description}}
</section>
<section class="container">
{% for day in days|reverse %}
<div class="data_header">{{ day }}</div>
{% endfor %}
{% for day in days|reverse %}
{% set corplist = collected.corpListByDate[day] %}
<div>{% for item in corplist %}
<div class="table_item"><a href="https://stockplus.com/m/stocks/KOREA-A{{ item.Code }}">{{ item.Name }}({{item.Code}})</a></div>{% endfor %}
</div>
{% endfor %}
</section>
</div>
</body>
</html>

View File

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock</title>
<style>
body{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(to right, #2b2b2b, #3d1844);
color: #fff;
}
.container{
display: grid;
background: #f0f0f0;
color: black;
box-shadow: 0px 0px 5px 0px white;
text-decoration: none;
}
.container a:link, a:visited{
text-decoration: none;
color: black;
font-size: 40px;
}
.data_header{
border-bottom: 1px solid #a7a7a7;
}
</style>
</head>
<body>
<div style="margin: auto; max-width: 750px;">
<h1>Main</h1>
<div class="container">
{% for p in pages %}
<a href="/dist/{{p.name}}.html">{{p.name}}</a>
{% endfor %}
</div>
</div>
</body>
</html>

View File

@ -1,5 +1,10 @@
import { Options } from "$fresh/plugins/twind.ts"; import { defineConfig, Preset } from "https://esm.sh/@twind/core@1.1.3";
import presetTailwind from "https://esm.sh/@twind/preset-tailwind@1.1.4";
import presetAutoprefix from "https://esm.sh/@twind/preset-autoprefix@1.0.7";
export default { export default {
...defineConfig({
presets: [presetTailwind() as Preset, presetAutoprefix()],
}),
selfURL: import.meta.url, selfURL: import.meta.url,
} as Options; }

38
util/api.ts Normal file
View File

@ -0,0 +1,38 @@
export interface Coperation {
Name: string;
Code: string;
Sector: string;
Product: string;
ListingDay: string;
ClosingMonth: string;
Representative: string;
Homepage: string;
AddressArea: string;
LastUpdate: string;
}
export interface PageCorpsInfo {
name: string;
description: string;
corpListByDate: Record<string, Coperation[]>;
}
export interface CorpSimple {
Code: string;
Name: string;
}
export async function fetchPageInfo(pageName: string): Promise<PageCorpsInfo>{
const res = await fetch("/api/pages/" + encodeURIComponent(pageName));
return await res.json();
}
export async function fetchKospiList(): Promise<CorpSimple[]>{
const res = await fetch("/api/kospi");
return await res.json();
}
export async function fetchKosdaqList(): Promise<CorpSimple[]> {
const res = await fetch("/api/kosdaq");
return await res.json();
}

35
util/util.ts Normal file
View File

@ -0,0 +1,35 @@
import { Signal, useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
export type QueryStatus<T> = {
type: "loading";
} | {
type: "complete";
data: T;
} | {
type: "error";
err: Error;
};
export function useAsync<T>(fn: () => Promise<T>): Signal<QueryStatus<T>> {
const state = useSignal({
type: "loading",
} as QueryStatus<T>);
useEffect(() => {
(async () => {
try {
const data = await fn();
state.value = {
type: "complete",
data: data,
};
} catch (err) {
state.value = {
type: "error",
err: err,
};
}
})();
}, []);
return state;
}